diff --git a/pom.xml b/pom.xml index 880d6df..fc42816 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,9 @@ - + + + diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 952fd22..7233496 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -58,6 +58,7 @@ 5.8.46 6.0.0-M22 1.3.0 + 5.3.3 2.4.1 2.0.63 33.6.0-jre @@ -506,6 +507,12 @@ + com.googlecode.aviator + aviator + ${aviator.version} + + + org.apache.tika tika-core ${tika-core.version} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java index b2db78b..c772692 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java @@ -4,8 +4,10 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import com.google.common.collect.ImmutableMap; +import java.math.BigDecimal; import java.util.*; import java.util.function.*; import java.util.stream.Collectors; @@ -33,6 +35,34 @@ public class CollectionUtils { return from.stream().anyMatch(predicate); } + public static long sum(Collection from, ToLongFunction valueFunc) { + if (CollUtil.isEmpty(from)) { + return 0L; + } + return from.stream().mapToLong(valueFunc).sum(); + } + + public static BigDecimal sumBigDecimal(Collection from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return BigDecimal.ZERO; + } + return from.stream().map(valueFunc).map(NumberUtils::zeroIfNull).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public static long count(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return 0L; + } + return from.stream().filter(predicate).count(); + } + + public static long distinctCount(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return 0L; + } + return from.stream().map(keyMapper).filter(Objects::nonNull).distinct().count(); + } + public static List filterList(Collection from, Predicate predicate) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); @@ -124,6 +154,15 @@ public class CollectionUtils { return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); } + public static > S convertSetBySupplier( + Collection from, Function func, Supplier supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return from.stream().map(func).filter(Objects::nonNull) + .collect(Collectors.toCollection(supplier)); + } + public static Set convertLinkedSet(Collection from, Function func) { if (CollUtil.isEmpty(from)) { return new LinkedHashSet<>(); @@ -398,4 +437,4 @@ public class CollectionUtils { return list; } -} \ No newline at end of file +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java index f942d6e..a78eda1 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java @@ -6,6 +6,9 @@ import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; import java.sql.Timestamp; import java.time.*; @@ -14,13 +17,16 @@ import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.TimeZone; +import java.util.stream.Collectors; import static cn.hutool.core.date.DatePattern.*; /** - * 时间工具类,用于 {@link LocalDateTime} + * 时间工具类,用于 {@link LocalDate}、{@link LocalDateTime} * * @author 芋道源码 */ @@ -54,6 +60,16 @@ public class LocalDateTimeUtils { } } + /** + * 解析年月字符串为 {@link YearMonth},格式为 yyyy-MM + * + * @param month 年月字符串 + * @return 年月 + */ + public static YearMonth parseYearMonth(String month) { + return YearMonth.parse(month, NORM_MONTH_FORMATTER); + } + public static LocalDateTime addTime(Duration duration) { return LocalDateTime.now().plus(duration); } @@ -70,6 +86,319 @@ public class LocalDateTimeUtils { return date.isAfter(LocalDateTime.now()); } + public static boolean beforeOrEqualNow(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + return time.isBefore(now) || time.isEqual(now); + } + + public static boolean beforeNow(LocalDate date) { + return date.isBefore(LocalDate.now()); + } + + public static boolean beforeOrEqualNow(LocalDate date) { + LocalDate today = LocalDate.now(); + return date.isBefore(today) || date.isEqual(today); + } + + public static boolean afterNow(LocalDate date) { + return date.isAfter(LocalDate.now()); + } + + /** + * 判断时间是否处于一月 + * + * @param time 时间 + * @return 是否处于一月 + */ + public static boolean isJanuary(LocalDateTime time) { + return time.getMonthValue() == 1; + } + + /** + * 判断第一个日期是否早于或等于第二个日期 + * + * @param firstDate 第一个日期 + * @param secondDate 第二个日期 + * @return 是否早于或等于 + */ + public static boolean isBeforeOrEqual(LocalDate firstDate, LocalDate secondDate) { + return firstDate.isBefore(secondDate) || firstDate.isEqual(secondDate); + } + + /** + * 判断第一个日期是否晚于或等于第二个日期 + * + * @param firstDate 第一个日期 + * @param secondDate 第二个日期 + * @return 是否晚于或等于 + */ + public static boolean isAfterOrEqual(LocalDate firstDate, LocalDate secondDate) { + return firstDate.isAfter(secondDate) || firstDate.isEqual(secondDate); + } + + /** + * 判断第一个时间是否早于或等于第二个时间 + * + * @param firstTime 第一个时间 + * @param secondTime 第二个时间 + * @return 是否早于或等于 + */ + public static boolean isBeforeOrEqual(LocalDateTime firstTime, LocalDateTime secondTime) { + return firstTime.isBefore(secondTime) || firstTime.isEqual(secondTime); + } + + /** + * 判断第一个时间是否晚于或等于第二个时间 + * + * @param firstTime 第一个时间 + * @param secondTime 第二个时间 + * @return 是否晚于或等于 + */ + public static boolean isAfterOrEqual(LocalDateTime firstTime, LocalDateTime secondTime) { + return firstTime.isAfter(secondTime) || firstTime.isEqual(secondTime); + } + + /** + * 判断时间范围是否有效 + * + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @return 开始、结束时间非空,且结束时间晚于开始时间时返回 {@code true} + */ + public static boolean isTimeRangeValid(LocalDateTime beginTime, LocalDateTime endTime) { + return beginTime != null && endTime != null && endTime.isAfter(beginTime); + } + + /** + * 判断时间范围是否存在有效边界 + * + * @param times 时间范围 + * @return 任一边界非空时返回 {@code true} + */ + public static boolean isTimeRangePresent(LocalDateTime[] times) { + return ArrayUtils.get(times, 0) != null || ArrayUtils.get(times, 1) != null; + } + + /** + * 判断指定日期是否位于闭区间内 + * + * @param beginDate 开始日期 + * @param endDate 结束日期 + * @param date 指定日期 + * @return 是否位于闭区间内 + */ + public static boolean isBetween(LocalDate beginDate, LocalDate endDate, LocalDate date) { + return beginDate != null && endDate != null && date != null + && isAfterOrEqual(date, beginDate) && isBeforeOrEqual(date, endDate); + } + + /** + * 判断指定日期是否不在闭区间内 + * + * @param beginDate 开始日期 + * @param endDate 结束日期 + * @param date 指定日期 + * @return 是否不在闭区间内;任一参数为空时返回 {@code true} + */ + public static boolean isNotBetween(LocalDate beginDate, LocalDate endDate, LocalDate date) { + return !isBetween(beginDate, endDate, date); + } + + /** + * 判断指定时间是否位于闭区间内 + * + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否位于闭区间内 + */ + public static boolean isBetween(LocalDateTime beginTime, LocalDateTime endTime, LocalDateTime time) { + return beginTime != null && endTime != null && time != null + && isAfterOrEqual(time, beginTime) && isBeforeOrEqual(time, endTime); + } + + /** + * 判断指定时间是否不在闭区间内 + * + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否不在闭区间内;任一参数为空时返回 {@code true} + */ + public static boolean isNotBetween(LocalDateTime beginTime, LocalDateTime endTime, LocalDateTime time) { + return !isBetween(beginTime, endTime, time); + } + + /** + * 计算两个日期之间经过的完整年数 + * + * @param beginDate 开始日期 + * @param endDate 结束日期 + * @return 完整年数;日期为空或开始日期晚于结束日期时,返回 0 + */ + public static int getYearsBetween(LocalDate beginDate, LocalDate endDate) { + if (beginDate == null || endDate == null || beginDate.isAfter(endDate)) { + return 0; + } + return Period.between(beginDate, endDate).getYears(); + } + + /** + * 从时间范围列表中扣除指定时间范围 + * + * @param sourceRanges 原时间范围列表 + * @param excludedRange 需要扣除的时间范围 + * @return 扣除后的时间范围列表 + */ + public static List subtractTimeRanges(List sourceRanges, TimeRange excludedRange) { + List result = new ArrayList<>(); + for (TimeRange sourceRange : sourceRanges) { + TimeRange overlap = intersectTimeRange(sourceRange, excludedRange); + if (overlap == null) { + result.add(sourceRange); + continue; + } + if (sourceRange.getStartTime().isBefore(overlap.getStartTime())) { + result.add(new TimeRange(sourceRange.getStartTime(), overlap.getStartTime())); + } + if (overlap.getEndTime().isBefore(sourceRange.getEndTime())) { + result.add(new TimeRange(overlap.getEndTime(), sourceRange.getEndTime())); + } + } + return result; + } + + /** + * 合并重叠或首尾相接的时间范围 + * + * @param timeRanges 时间范围列表 + * @return 合并后的时间范围列表 + */ + public static List mergeTimeRanges(List timeRanges) { + if (CollUtil.isEmpty(timeRanges)) { + return Collections.emptyList(); + } + List sortedRanges = timeRanges.stream() + .sorted(Comparator.comparing(TimeRange::getStartTime)) + .collect(Collectors.toList()); + List result = new ArrayList<>(); + TimeRange currentRange = CollUtil.getFirst(sortedRanges); + for (int i = 1; i < sortedRanges.size(); i++) { + TimeRange nextRange = sortedRanges.get(i); + if (nextRange.getStartTime().isAfter(currentRange.getEndTime())) { + result.add(currentRange); + currentRange = nextRange; + continue; + } + LocalDateTime endTime = currentRange.getEndTime().isAfter(nextRange.getEndTime()) + ? currentRange.getEndTime() : nextRange.getEndTime(); + currentRange = new TimeRange(currentRange.getStartTime(), endTime); + } + result.add(currentRange); + return result; + } + + /** + * 计算两个时间范围列表的交集 + * + * @param firstRanges 第一组时间范围 + * @param secondRanges 第二组时间范围 + * @return 合并后的交集时间范围列表 + */ + public static List intersectTimeRanges( + List firstRanges, List secondRanges) { + List result = new ArrayList<>(); + for (TimeRange firstRange : firstRanges) { + for (TimeRange secondRange : secondRanges) { + TimeRange overlap = intersectTimeRange(firstRange, secondRange); + if (overlap != null) { + result.add(overlap); + } + } + } + return mergeTimeRanges(result); + } + + /** + * 计算两个时间范围的交集 + * + * @param firstRange 第一个时间范围 + * @param secondRange 第二个时间范围 + * @return 交集时间范围;没有交集时返回 {@code null} + */ + public static TimeRange intersectTimeRange(TimeRange firstRange, TimeRange secondRange) { + LocalDateTime startTime = firstRange.getStartTime().isAfter(secondRange.getStartTime()) + ? firstRange.getStartTime() : secondRange.getStartTime(); + LocalDateTime endTime = firstRange.getEndTime().isBefore(secondRange.getEndTime()) + ? firstRange.getEndTime() : secondRange.getEndTime(); + return endTime.isAfter(startTime) ? new TimeRange(startTime, endTime) : null; + } + + /** + * 计算时间范围的总分钟数 + * + * @param timeRanges 时间范围列表 + * @return 总分钟数 + */ + public static int calculateDurationMinutes(List timeRanges) { + return timeRanges.stream() + .mapToInt(range -> Math.toIntExact(Duration.between( + range.getStartTime(), range.getEndTime()).toMinutes())) + .sum(); + } + + /** + * 构建日期范围内每天对应的时间范围 + * + * 当结束时刻早于或等于开始时刻时,按跨天时间范围处理。 + * + * @param beginDate 开始日期 + * @param endDate 结束日期 + * @param beginTime 每日开始时刻 + * @param endTime 每日结束时刻 + * @return 每日时间范围列表 + */ + public static List buildDailyTimeRanges( + LocalDate beginDate, LocalDate endDate, LocalTime beginTime, LocalTime endTime) { + if (beginDate == null || endDate == null || beginTime == null || endTime == null + || beginDate.isAfter(endDate)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (LocalDate date = beginDate; isBeforeOrEqual(date, endDate); date = date.plusDays(1)) { + LocalDateTime rangeBeginTime = date.atTime(beginTime); + LocalDateTime rangeEndTime = date.atTime(endTime); + if (rangeEndTime.isBefore(rangeBeginTime) || rangeEndTime.isEqual(rangeBeginTime)) { + rangeEndTime = rangeEndTime.plusDays(1); + } + result.add(new TimeRange(rangeBeginTime, rangeEndTime)); + } + return result; + } + + /** + * 查找包含指定时刻的每日时间范围 + * + * 每日时间范围允许跨天,因此会同时检查基准日期的前一天、当天和后一天。 + * 范围匹配使用左右闭区间。 + * + * @param date 基准日期 + * @param beginTime 每日开始时刻 + * @param endTime 每日结束时刻 + * @param time 指定时刻 + * @return 包含指定时刻的时间范围;不存在时返回 {@code null} + */ + public static TimeRange findDailyTimeRange( + LocalDate date, LocalTime beginTime, LocalTime endTime, LocalDateTime time) { + if (date == null || time == null) { + return null; + } + List timeRanges = buildDailyTimeRanges( + date.minusDays(1), date.plusDays(1), beginTime, endTime); + return CollUtil.findOne(timeRanges, timeRange -> isBetween( + timeRange.getStartTime(), timeRange.getEndTime(), time)); + } + /** * 将 Unix 秒时间戳转换为默认时区的本地时间 * @@ -185,6 +514,62 @@ public class LocalDateTimeUtils { } /** + * 获得指定日期的开始时间 + * + * @param date 日期 + * @return 当日开始时间;日期为空时返回 {@code null} + */ + public static LocalDateTime getDayBeginTime(LocalDate date) { + return date == null ? null : date.atStartOfDay(); + } + + /** + * 获得指定时间所在日期的开始时间 + * + * @param time 时间 + * @return 当日开始时间;时间为空时返回 {@code null} + */ + public static LocalDateTime getDayBeginTime(LocalDateTime time) { + return time == null ? null : getDayBeginTime(time.toLocalDate()); + } + + /** + * 获得指定日期的结束时间 + * + * @param date 日期 + * @return 当日结束时间;日期为空时返回 {@code null} + */ + public static LocalDateTime getDayEndTime(LocalDate date) { + return date == null ? null : date.atTime(LocalTime.MAX); + } + + /** + * 获得自然日期范围对应的日期时间闭区间 + * + * @param beginDate 开始日期 + * @param endDate 结束日期 + * @return 从开始日期零点到结束日期最后时刻的闭区间;两个日期都为空时返回 {@code null} + */ + public static LocalDateTime[] getDateTimeRange(LocalDate beginDate, LocalDate endDate) { + if (beginDate == null && endDate == null) { + return null; + } + return new LocalDateTime[]{getDayBeginTime(beginDate), getDayEndTime(endDate)}; + } + + /** + * 获得指定月份的日期时间闭区间 + * + * @param year 年份 + * @param month 月份 + * @return 从当月月初零点到月末最后时刻的闭区间 + */ + public static LocalDateTime[] getMonthDateTimeRange(int year, int month) { + LocalDate monthBeginDate = LocalDate.of(year, month, 1); + return getDateTimeRange(monthBeginDate, monthBeginDate.with(TemporalAdjusters.lastDayOfMonth())); + } + + /** * 获取指定日期所在的月份的开始时间 * 例如:2023-09-30 00:00:00,000 * @@ -196,6 +581,16 @@ public class LocalDateTimeUtils { } /** + * 获取指定日期的开始时间 + * + * @param date 日期 + * @return 当日开始时间 + */ + public static LocalDateTime beginOfDay(LocalDateTime date) { + return date.with(LocalTime.MIN); + } + + /** * 获取指定日期所在的月份的最后时间 * 例如:2023-09-30 23:59:59,999 * @@ -207,6 +602,82 @@ public class LocalDateTimeUtils { } /** + * 获得指定年月的月初时间 + * + * @param year 年份 + * @param month 月份 + * @return 月初时间 + */ + public static LocalDateTime getMonthBeginTime(int year, int month) { + return LocalDateTime.of(year, month, 1, 0, 0); + } + + /** + * 获得指定年月的月初时间 + * + * @param month 年月 + * @return 月初时间 + */ + public static LocalDateTime getMonthBeginTime(YearMonth month) { + return getMonthBeginTime(month.getYear(), month.getMonthValue()); + } + + /** + * 获得指定年月的下月月初时间 + * + * @param year 年份 + * @param month 月份 + * @return 下月月初时间 + */ + public static LocalDateTime getNextMonthBeginTime(int year, int month) { + return getMonthBeginTime(year, month).plusMonths(1); + } + + /** + * 获得指定年月的下月月初时间 + * + * @param month 年月 + * @return 下月月初时间 + */ + public static LocalDateTime getNextMonthBeginTime(YearMonth month) { + return getMonthBeginTime(month).plusMonths(1); + } + + /** + * 判断两个时间段是否存在正时长的重叠 + * + * @param beginTime1 第一个范围的开始时间 + * @param endTime1 第一个范围的结束时间 + * @param beginTime2 第二个范围的开始时间 + * @param endTime2 第二个范围的结束时间 + * @return 是否存在正时长的重叠 + */ + public static boolean isOverlap(LocalDateTime beginTime1, LocalDateTime endTime1, + LocalDateTime beginTime2, LocalDateTime endTime2) { + if (beginTime1 == null || endTime1 == null || beginTime2 == null || endTime2 == null) { + return false; + } + return beginTime1.isBefore(endTime2) && endTime1.isAfter(beginTime2); + } + + /** + * 判断两个左右闭合的时间范围是否重叠 + * + * @param beginTime1 第一个范围的开始时间 + * @param endTime1 第一个范围的结束时间 + * @param beginTime2 第二个范围的开始时间 + * @param endTime2 第二个范围的结束时间 + * @return 是否重叠 + */ + public static boolean isClosedRangeOverlap(LocalDateTime beginTime1, LocalDateTime endTime1, + LocalDateTime beginTime2, LocalDateTime endTime2) { + if (beginTime1 == null || endTime1 == null || beginTime2 == null || endTime2 == null) { + return false; + } + return isBeforeOrEqual(beginTime1, endTime2) && isAfterOrEqual(endTime1, beginTime2); + } + + /** * 获得指定日期所在季度 * * @param date 日期 @@ -353,7 +824,11 @@ public class LocalDateTimeUtils { * @return 日期列表,包含开始日期 */ public static List getDateList(LocalDate startDate, int days) { - return startDate.datesUntil(startDate.plusDays(days)).toList(); + List dateList = new ArrayList<>(days); + for (int i = 0; i < days; i++) { + dateList.add(startDate.plusDays(i)); + } + return dateList; } /** @@ -433,4 +908,23 @@ public class LocalDateTimeUtils { return sourceDateTime.atZone(zoneId).toEpochSecond(); } + /** + * 时间范围 + */ + @Getter + @AllArgsConstructor + @SuppressWarnings("ClassCanBeRecord") + public static class TimeRange { + + /** + * 开始时间 + */ + private final LocalDateTime startTime; + /** + * 结束时间 + */ + private final LocalDateTime endTime; + + } + } diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java index 90888d7..7028151 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java @@ -97,6 +97,39 @@ public class MoneyUtils { } /** + * 格式化金额,默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE}。金额为空时,按 0 处理。 + * + * @param price 金额 + * @return 格式化后的金额 + */ + public static BigDecimal priceScale(BigDecimal price) { + return (price == null ? BigDecimal.ZERO : price).setScale(PRICE_SCALE, RoundingMode.HALF_UP); + } + + /** + * 金额相加,默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE}。忽略空金额,没有有效金额时返回 0。 + * + * @param prices 金额数组 + * @return 金额相加结果 + */ + public static BigDecimal priceAdd(BigDecimal... prices) { + BigDecimal result = BigDecimal.ZERO; + if (prices == null) { + return priceScale(result); + } + for (BigDecimal price : prices) { + if (price != null) { + result = result.add(price); + } + } + return priceScale(result); + } + + /** * 金额相乘,默认进行四舍五入 * * 位数:{@link #PRICE_SCALE} @@ -109,7 +142,7 @@ public class MoneyUtils { if (price == null || count == null) { return null; } - return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP); + return priceScale(price.multiply(count)); } /** diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java index c2787f4..544a797 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java @@ -2,10 +2,12 @@ package cn.iocoder.yudao.framework.common.util.number; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import java.math.BigDecimal; import java.util.List; +import java.util.regex.Pattern; /** * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 @@ -14,6 +16,13 @@ import java.util.List; */ public class NumberUtils { + private static final Pattern FIRST_BIG_DECIMAL_PATTERN = Pattern.compile("-?\\d+(?:\\.\\d+)?"); + + /** + * 一百,用于百分数和比例计算 + */ + public static final BigDecimal ONE_HUNDRED = new BigDecimal("100"); + public static Long parseLong(String str) { return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; } @@ -22,6 +31,30 @@ public class NumberUtils { return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; } + /** + * 获得数字,为 null 时返回 {@link BigDecimal#ZERO} + * + * @param value 数字 + * @return 数字 + */ + public static BigDecimal zeroIfNull(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + /** + * 从文本中解析首个数字 + * + * @param text 文本 + * @return 数字;不存在时返回 {@link BigDecimal#ZERO} + */ + public static BigDecimal parseFirstBigDecimal(String text) { + if (StrUtil.isEmpty(text)) { + return BigDecimal.ZERO; + } + String number = ReUtil.getGroup0(FIRST_BIG_DECIMAL_PATTERN, text.replace(",", "")); + return NumberUtil.toBigDecimal(number); + } + public static boolean isAllNumber(List values) { if (CollUtil.isEmpty(values)) { return false; diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java index cf077db..472fff8 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java @@ -5,10 +5,13 @@ import cn.hutool.core.lang.func.Func1; import cn.hutool.core.lang.func.LambdaUtil; import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; import cn.iocoder.yudao.framework.common.pojo.SortingField; import org.springframework.util.Assert; +import java.util.List; + import static java.util.Collections.singletonList; /** @@ -25,6 +28,27 @@ public class PageUtils { } /** + * 对内存列表进行分页 + * + * @param list 数据列表 + * @param pageParam 分页参数 + * @param 数据类型 + * @return 分页结果 + */ + public static PageResult buildPageResult(List list, PageParam pageParam) { + if (CollUtil.isEmpty(list)) { + return PageResult.empty(); + } + long total = list.size(); + if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { + return new PageResult<>(list, total); + } + int fromIndex = Math.toIntExact(Math.min(getStart(pageParam), total)); + int toIndex = Math.toIntExact(Math.min((long) fromIndex + pageParam.getPageSize(), total)); + return new PageResult<>(list.subList(fromIndex, toIndex), total); + } + + /** * 构建排序字段(默认倒序) * * @param func 排序字段的 Lambda 表达式 diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java index 905f5d5..3501bcc 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.servlet; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; @@ -11,6 +12,7 @@ import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import java.io.IOException; import java.util.Map; /** @@ -102,4 +104,11 @@ public class ServletUtils { return JakartaServletUtil.getHeaderMap(request); } + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) + throws IOException { + response.getOutputStream().write(content); + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + } + } diff --git a/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java b/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java index 0e44645..f0aaa63 100644 --- a/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java +++ b/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java @@ -6,7 +6,9 @@ import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.TreeSet; import java.util.function.BiFunction; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,6 +29,38 @@ public class CollectionUtilsTest { } @Test + public void testSum() { + assertEquals(6L, CollectionUtils.sum(Arrays.asList(1L, 2L, 3L), Long::longValue)); + assertEquals(0L, CollectionUtils.sum(Collections.emptyList(), Long::longValue)); + } + + @Test + public void testCount() { + assertEquals(2L, CollectionUtils.count(Arrays.asList(1, 2, 3, 4), value -> value % 2 == 0)); + assertEquals(0L, CollectionUtils.count(Collections.emptyList(), value -> true)); + } + + @Test + public void testDistinctCount() { + assertEquals(3L, CollectionUtils.distinctCount(Arrays.asList("a", "b", "a", "c"), value -> value)); + assertEquals(0L, CollectionUtils.distinctCount(Collections.emptyList(), value -> value)); + } + + @Test + public void testConvertSet_withSupplier() { + // 调用,并断言:按指定 Set 类型完成转换、去重和排序 + TreeSet result = CollectionUtils.convertSetBySupplier( + Arrays.asList("3", "1", null, "3", "2"), + value -> value == null ? null : Integer.valueOf(value), TreeSet::new); + assertEquals(Arrays.asList(1, 2, 3), Arrays.asList(result.toArray(new Integer[0]))); + + // 调用,并断言:空集合仍返回指定 Set 类型 + result = CollectionUtils.convertSetBySupplier(Collections.emptyList(), + Integer::valueOf, TreeSet::new); + assertEquals(TreeSet.class, result.getClass()); + } + + @Test public void testDiffList() { // 准备参数 Collection oldList = Arrays.asList( diff --git a/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtilsTest.java b/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtilsTest.java new file mode 100644 index 0000000..abb7a66 --- /dev/null +++ b/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtilsTest.java @@ -0,0 +1,342 @@ +package cn.iocoder.yudao.framework.common.util.date; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link LocalDateTimeUtils} 的单元测试 + * + * @author 芋道源码 + */ +public class LocalDateTimeUtilsTest { + + @Test + public void testDateCompare() { + // 准备参数 + LocalDate firstDate = LocalDate.of(2026, 7, 23); + LocalDate secondDate = LocalDate.of(2026, 7, 24); + + // 调用,并断言 + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstDate, secondDate)); + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondDate, secondDate)); + assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondDate, firstDate)); + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, firstDate)); + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, secondDate)); + assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstDate, secondDate)); + } + + @Test + public void testDateTimeCompare() { + // 准备参数 + LocalDateTime firstTime = LocalDateTime.of(2026, 7, 24, 9, 0); + LocalDateTime secondTime = LocalDateTime.of(2026, 7, 24, 18, 0); + + // 调用,并断言 + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstTime, secondTime)); + assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondTime, secondTime)); + assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondTime, firstTime)); + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, firstTime)); + assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, secondTime)); + assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstTime, secondTime)); + } + + @Test + public void testIsTimeRangeValid() { + // 准备参数 + LocalDateTime beginTime = LocalDateTime.of(2026, 7, 24, 9, 0); + LocalDateTime endTime = LocalDateTime.of(2026, 7, 24, 18, 0); + + // 调用,并断言 + assertTrue(LocalDateTimeUtils.isTimeRangeValid(beginTime, endTime)); + assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, beginTime)); + assertFalse(LocalDateTimeUtils.isTimeRangeValid(endTime, beginTime)); + assertFalse(LocalDateTimeUtils.isTimeRangeValid(null, endTime)); + assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, null)); + } + + @Test + public void testIsTimeRangePresent() { + // 准备参数 + LocalDateTime time = LocalDateTime.of(2026, 7, 24, 9, 0); + + // 调用,并断言:空数组和双空边界都不是有效范围 + assertFalse(LocalDateTimeUtils.isTimeRangePresent(null)); + assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[0])); + assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, null})); + + // 调用,并断言:单边或双边存在即为有效范围 + assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time})); + assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, time})); + assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time, time.plusHours(1)})); + } + + @Test + public void testGetDayTime() { + // 准备参数 + LocalDate date = LocalDate.of(2026, 7, 24); + LocalDateTime time = date.atTime(15, 30); + + // 调用,并断言 + assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0), + LocalDateTimeUtils.getDayBeginTime(date)); + assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0), + LocalDateTimeUtils.getDayBeginTime(time)); + assertNull(LocalDateTimeUtils.getDayBeginTime((LocalDateTime) null)); + assertEquals(LocalDateTime.of(date, LocalTime.MAX), + LocalDateTimeUtils.getDayEndTime(date)); + } + + @Test + public void testGetDateTimeRange() { + // 准备参数 + LocalDate beginDate = LocalDate.of(2026, 7, 1); + LocalDate endDate = LocalDate.of(2026, 7, 31); + + // 调用,并断言:左右两端均为闭区间边界 + LocalDateTime[] result = LocalDateTimeUtils.getDateTimeRange(beginDate, endDate); + assertEquals(beginDate.atStartOfDay(), result[0]); + assertEquals(endDate.atTime(LocalTime.MAX), result[1]); + + // 调用,并断言:支持单边范围 + result = LocalDateTimeUtils.getDateTimeRange(beginDate, null); + assertEquals(beginDate.atStartOfDay(), result[0]); + assertNull(result[1]); + result = LocalDateTimeUtils.getDateTimeRange(null, endDate); + assertNull(result[0]); + assertEquals(endDate.atTime(LocalTime.MAX), result[1]); + + // 调用,并断言:两个边界都为空时不生成范围 + assertNull(LocalDateTimeUtils.getDateTimeRange(null, null)); + } + + @Test + public void testGetMonthDateTimeRange() { + // 调用:闰年二月 + LocalDateTime[] result = LocalDateTimeUtils.getMonthDateTimeRange(2024, 2); + + // 断言:完整包含闰年二月的首尾时刻 + assertEquals(LocalDateTime.of(2024, 2, 1, 0, 0), result[0]); + assertEquals(LocalDate.of(2024, 2, 29).atTime(LocalTime.MAX), result[1]); + } + + @Test + public void testGetMonthBeginTime() { + // 准备参数 + YearMonth month = YearMonth.of(2026, 12); + + // 调用,并断言 + assertEquals(LocalDateTime.of(2026, 12, 1, 0, 0), + LocalDateTimeUtils.getMonthBeginTime(month)); + assertEquals(LocalDateTime.of(2027, 1, 1, 0, 0), + LocalDateTimeUtils.getNextMonthBeginTime(month)); + } + + @Test + public void testIsBetween() { + // 准备参数 + LocalDate beginDate = LocalDate.of(2026, 7, 23); + LocalDate endDate = LocalDate.of(2026, 7, 25); + LocalDateTime beginTime = beginDate.atTime(9, 0); + LocalDateTime endTime = beginDate.atTime(18, 0); + + // 调用,并断言:日期闭区间 + assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, beginDate)); + assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 24))); + assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, endDate)); + assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 26))); + assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, null)); + assertFalse(LocalDateTimeUtils.isNotBetween(beginDate, endDate, beginDate)); + assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, LocalDate.of(2026, 7, 26))); + assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, null)); + + // 调用,并断言:时间闭区间 + assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginTime)); + assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(12, 0))); + assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, endTime)); + assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(18, 1))); + assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, (LocalDateTime) null)); + assertFalse(LocalDateTimeUtils.isNotBetween(beginTime, endTime, endTime)); + assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, beginDate.atTime(18, 1))); + assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, null)); + } + + @Test + public void testIsClosedRangeOverlap() { + // 准备参数 + LocalDateTime firstBeginTime = LocalDateTime.of(2026, 7, 24, 9, 0); + LocalDateTime firstEndTime = LocalDateTime.of(2026, 7, 24, 12, 0); + + // 调用,并断言:首尾相接属于闭区间重叠 + assertTrue(LocalDateTimeUtils.isClosedRangeOverlap( + firstBeginTime, firstEndTime, firstEndTime, firstEndTime.plusHours(1))); + assertTrue(LocalDateTimeUtils.isClosedRangeOverlap( + firstBeginTime, firstEndTime, firstBeginTime.minusHours(1), firstBeginTime)); + assertFalse(LocalDateTimeUtils.isClosedRangeOverlap( + firstBeginTime, firstEndTime, firstEndTime.plusNanos(1), firstEndTime.plusHours(1))); + assertFalse(LocalDateTimeUtils.isClosedRangeOverlap( + firstBeginTime, firstEndTime, null, firstEndTime)); + } + + @Test + public void testSubtractTimeRanges() { + // 准备参数 + LocalDate date = LocalDate.of(2026, 7, 24); + TimeRange sourceRange = new TimeRange(date.atTime(9, 0), date.atTime(18, 0)); + + // 调用:扣除原时段中间的一段 + List result = LocalDateTimeUtils.subtractTimeRanges( + Collections.singletonList(sourceRange), + new TimeRange(date.atTime(12, 0), date.atTime(13, 0))); + + // 断言:原时段被拆分为两段 + assertEquals(2, result.size()); + assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(12, 0)); + assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(18, 0)); + + // 调用,并断言:扣除时段完整覆盖原时段 + result = LocalDateTimeUtils.subtractTimeRanges( + Collections.singletonList(sourceRange), + new TimeRange(date.atTime(8, 0), date.atTime(19, 0))); + assertTrue(result.isEmpty()); + + // 调用:扣除时段与原时段首尾相接 + result = LocalDateTimeUtils.subtractTimeRanges( + Collections.singletonList(sourceRange), + new TimeRange(date.atTime(18, 0), date.atTime(19, 0))); + // 断言:两个时段仅首尾相接,没有正时长交集,原时段保持不变 + assertEquals(1, result.size()); + assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(18, 0)); + } + + @Test + public void testMergeTimeRanges() { + // 准备参数 + LocalDate date = LocalDate.of(2026, 7, 24); + List timeRanges = Arrays.asList( + new TimeRange(date.atTime(13, 0), date.atTime(14, 0)), + new TimeRange(date.atTime(9, 0), date.atTime(11, 0)), + new TimeRange(date.atTime(10, 0), date.atTime(13, 0)), + new TimeRange(date.atTime(16, 0), date.atTime(17, 0))); + + // 调用 + List result = LocalDateTimeUtils.mergeTimeRanges(timeRanges); + + // 断言:重叠或首尾相接的时段被合并,独立时段保持不变 + assertEquals(2, result.size()); + assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(14, 0)); + assertTimeRange(result.get(1), date.atTime(16, 0), date.atTime(17, 0)); + // 调用,并断言:空列表 + assertTrue(LocalDateTimeUtils.mergeTimeRanges(Collections.emptyList()).isEmpty()); + } + + @Test + public void testIntersectTimeRanges() { + // 准备参数 + LocalDate date = LocalDate.of(2026, 7, 24); + List firstRanges = Arrays.asList( + new TimeRange(date.atTime(9, 0), date.atTime(12, 0)), + new TimeRange(date.atTime(13, 0), date.atTime(18, 0))); + List secondRanges = Arrays.asList( + new TimeRange(date.atTime(10, 0), date.atTime(14, 0)), + new TimeRange(date.atTime(17, 0), date.atTime(19, 0))); + + // 调用 + List result = LocalDateTimeUtils.intersectTimeRanges(firstRanges, secondRanges); + + // 断言 + assertEquals(3, result.size()); + assertTimeRange(CollUtil.getFirst(result), date.atTime(10, 0), date.atTime(12, 0)); + assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(14, 0)); + assertTimeRange(result.get(2), date.atTime(17, 0), date.atTime(18, 0)); + // 调用,并断言:两个时段仅首尾相接时不生成零时长交集 + assertNull(LocalDateTimeUtils.intersectTimeRange( + new TimeRange(date.atTime(9, 0), date.atTime(10, 0)), + new TimeRange(date.atTime(10, 0), date.atTime(11, 0)))); + } + + @Test + public void testCalculateDurationMinutes() { + // 准备参数 + LocalDate date = LocalDate.of(2026, 7, 24); + List timeRanges = Arrays.asList( + new TimeRange(date.atTime(9, 0), date.atTime(10, 0)), + new TimeRange(date.atTime(13, 0), date.atTime(14, 30))); + + // 调用,并断言 + assertEquals(150, LocalDateTimeUtils.calculateDurationMinutes(timeRanges)); + // 调用,并断言:空列表 + assertEquals(0, LocalDateTimeUtils.calculateDurationMinutes(Collections.emptyList())); + } + + @Test + public void testBuildDailyTimeRanges() { + // 准备参数 + LocalDate beginDate = LocalDate.of(2026, 7, 24); + LocalDate endDate = LocalDate.of(2026, 7, 25); + + // 调用:构建跨天时间范围 + List result = LocalDateTimeUtils.buildDailyTimeRanges( + beginDate, endDate, LocalTime.of(22, 0), LocalTime.of(6, 0)); + + // 断言 + assertEquals(2, result.size()); + assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(22, 0), beginDate.plusDays(1).atTime(6, 0)); + assertTimeRange(result.get(1), endDate.atTime(22, 0), endDate.plusDays(1).atTime(6, 0)); + + // 调用:每日开始和结束时刻相同 + result = LocalDateTimeUtils.buildDailyTimeRanges( + beginDate, beginDate, LocalTime.of(9, 0), LocalTime.of(9, 0)); + // 断言:按完整一天处理 + assertEquals(1, result.size()); + assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(9, 0), beginDate.plusDays(1).atTime(9, 0)); + + // 调用,并断言:开始日期晚于结束日期 + assertTrue(LocalDateTimeUtils.buildDailyTimeRanges( + endDate, beginDate, LocalTime.of(9, 0), LocalTime.of(18, 0)).isEmpty()); + } + + @Test + public void testFindDailyTimeRange() { + // 准备参数 + LocalDate date = LocalDate.of(2026, 7, 8); + + // 调用,并断言普通时间范围的左右边界 + TimeRange result = LocalDateTimeUtils.findDailyTimeRange( + date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(8, 0)); + assertEquals(date.atTime(8, 0), result.getStartTime()); + assertEquals(date.atTime(10, 0), result.getEndTime()); + assertNotNull(LocalDateTimeUtils.findDailyTimeRange( + date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(10, 0))); + + // 调用,并断言跨日时间范围 + result = LocalDateTimeUtils.findDailyTimeRange( + date, LocalTime.of(20, 0), LocalTime.of(6, 0), date.plusDays(1).atTime(5, 0)); + assertEquals(date.atTime(20, 0), result.getStartTime()); + assertEquals(date.plusDays(1).atTime(6, 0), result.getEndTime()); + assertNull(LocalDateTimeUtils.findDailyTimeRange( + date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(12, 0))); + } + + private static void assertTimeRange(TimeRange timeRange, + LocalDateTime expectedStartTime, + LocalDateTime expectedEndTime) { + assertEquals(expectedStartTime, timeRange.getStartTime()); + assertEquals(expectedEndTime, timeRange.getEndTime()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/StringListConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/StringListConvert.java new file mode 100644 index 0000000..edb243e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/StringListConvert.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import cn.hutool.core.collection.CollUtil; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; + +import java.util.List; + +/** + * Excel 字符串列表转换器 + * + * @author 芋道源码 + */ +public class StringListConvert implements Converter> { + + @Override + public WriteCellData convertToExcelData(List value, + ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(CollUtil.isEmpty(value) ? "" : String.join("/", value)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java index f4f17ec..d771d18 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.framework.excel.core.util; import cn.idev.excel.FastExcelFactory; +import cn.idev.excel.context.AnalysisContext; import cn.idev.excel.converters.longconverter.LongStringConverter; +import cn.idev.excel.read.listener.ReadListener; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy; import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; @@ -10,7 +12,10 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; /** * Excel 工具类 @@ -44,7 +49,33 @@ public class ExcelUtils { response.setContentType("application/vnd.ms-excel;charset=UTF-8"); } + /** + * 将动态表头和数据以 Excel 响应给前端 + * + * @param response 响应 + * @param filename 文件名 + * @param sheetName Excel sheet 名 + * @param head Excel head 头 + * @param data 数据列表 + * @throws IOException 写入失败的情况 + */ + public static void write(HttpServletResponse response, String filename, String sheetName, + List> head, List> data) throws IOException { + // 输出 Excel + FastExcelFactory.write(response.getOutputStream()) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 + .head(head).sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + response.setContentType("application/vnd.ms-excel;charset=UTF-8"); + } + public static List read(MultipartFile file, Class head) throws IOException { + if (file == null || file.isEmpty()) { + return Collections.emptyList(); + } // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景 try (InputStream inputStream = file.getInputStream()) { return FastExcelFactory.read(inputStream, head, null) @@ -53,4 +84,64 @@ public class ExcelUtils { } } + /** + * 读取指定数量以内的 Excel 数据,达到上限后停止继续解析。 + * + * @param file Excel 文件,可为空 + * @param head Excel 表头类型 + * @param maxRowCount 最大读取行数 + * @return Excel 数据 + */ + public static List read(MultipartFile file, Class head, int maxRowCount) throws IOException { + if (file == null || file.isEmpty()) { + return Collections.emptyList(); + } + if (maxRowCount <= 0) { + throw new IllegalArgumentException("maxRowCount must be greater than 0"); + } + List rows = new ArrayList<>(Math.min(maxRowCount, 1024)); + try (InputStream inputStream = file.getInputStream()) { + FastExcelFactory.read(inputStream, head, new ReadListener() { + + @Override + public void invoke(T data, AnalysisContext context) { + rows.add(data); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 无需处理 + } + + @Override + public boolean hasNext(AnalysisContext context) { + return rows.size() < maxRowCount; + } + + }).autoCloseStream(false).doReadAll(); + } + return rows; + } + + /** + * 读取动态表头 Excel 数据 + * + * @param file Excel 文件,可为空 + * @return 按列下标存储的行数据 + * @throws IOException 读取失败 + */ + public static List> read(MultipartFile file) throws IOException { + if (file == null || file.isEmpty()) { + return Collections.emptyList(); + } + // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景 + try (InputStream inputStream = file.getInputStream()) { + return FastExcelFactory.read(inputStream) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .headRowNumber(1) + .sheet() + .doReadSync(); + } + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/PoiExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/PoiExcelUtils.java new file mode 100644 index 0000000..933d348 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/PoiExcelUtils.java @@ -0,0 +1,226 @@ +package cn.iocoder.yudao.framework.excel.core.util; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import lombok.experimental.UtilityClass; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; + +import java.math.BigDecimal; +import java.util.Collection; + +/** + * 原生 POI 的 Excel 工具类 + * + * 全局 {@link ExcelUtils}(FastExcel)无法表达单元格锁定、公式、动态列等复杂模板时,各模块基于本类实现自定义 Excel 读写 + * + * @author 芋道源码 + */ +@UtilityClass +public class PoiExcelUtils { + + /** + * 读取公式计算值必须 DataFormatter + FormulaEvaluator,hutool 没有更简的等价写法 + */ + private static final DataFormatter DATA_FORMATTER = new DataFormatter(); + + // ==================== 读取 ==================== + + /** + * 读取单元格文本,公式单元格取公式计算值 + * + * @param row 行 + * @param column 列 + * @param evaluator 公式计算器,不需要求值时传 null + * @return 文本,空单元格返回空字符串 + */ + public static String getCellText(Row row, Integer column, FormulaEvaluator evaluator) { + if (row == null || column == null || row.getCell(column) == null) { + return ""; + } + return evaluator == null + ? DATA_FORMATTER.formatCellValue(row.getCell(column)) + : DATA_FORMATTER.formatCellValue(row.getCell(column), evaluator); + } + + /** + * 读取单元格数字,兼容千分位文本 + * + * @param row 行 + * @param column 列 + * @param evaluator 公式计算器,不需要求值时传 null + * @return 数字,空单元格返回 null;格式不正确时抛出 {@link NumberFormatException} + */ + public static BigDecimal getDecimal(Row row, Integer column, FormulaEvaluator evaluator) { + String value = getCellText(row, column, evaluator).replace(",", ""); + return StrUtil.isBlank(value) ? null : NumberUtil.toBigDecimal(value); + } + + /** + * 判断指定列是否全部为空 + * + * @param row 行 + * @param evaluator 公式计算器,不需要求值时传 null + * @param columns 列数组 + * @return 是否全部为空 + */ + public static boolean isEmptyRow(Row row, FormulaEvaluator evaluator, Collection columns) { + if (row == null) { + return true; + } + for (Integer column : columns) { + if (StrUtil.isNotBlank(getCellText(row, column, evaluator))) { + return false; + } + } + return true; + } + + // ==================== 写入 ==================== + + /** + * 写入文本单元格,null 按空字符串处理 + */ + public static void writeText(Row row, int column, String value, CellStyle style) { + Cell cell = row.createCell(column); + cell.setCellValue(StrUtil.nullToEmpty(value)); + cell.setCellStyle(style); + } + + /** + * 写入数字单元格,null 时留空 + */ + public static void writeNumber(Row row, int column, BigDecimal value, CellStyle style) { + Cell cell = row.createCell(column); + if (value != null) { + cell.setCellValue(value.doubleValue()); + } + cell.setCellStyle(style); + } + + /** + * 写入公式单元格 + */ + public static void writeFormula(Row row, int column, String formula, CellStyle style) { + Cell cell = row.createCell(column); + cell.setCellFormula(formula); + cell.setCellStyle(style); + } + + /** + * 写入跨两行的合并表头 + */ + public static void mergeHeader(Sheet sheet, Row firstRow, Row secondRow, + int beginColumn, int endColumn, String title, CellStyle style) { + sheet.addMergedRegion(new CellRangeAddress(firstRow.getRowNum(), secondRow.getRowNum(), + beginColumn, endColumn)); + Cell cell = firstRow.createCell(beginColumn); + cell.setCellValue(title); + cell.setCellStyle(style); + secondRow.createCell(beginColumn).setCellStyle(style); + } + + /** + * 添加单元格批注 + * + * @param cell 单元格 + * @param text 批注内容 + */ + public static void addComment(Cell cell, String text) { + CreationHelper helper = cell.getSheet().getWorkbook().getCreationHelper(); + ClientAnchor anchor = helper.createClientAnchor(); + anchor.setCol1(cell.getColumnIndex()); + anchor.setCol2(cell.getColumnIndex() + 3); + anchor.setRow1(cell.getRowIndex()); + anchor.setRow2(cell.getRowIndex() + 3); + Comment comment = cell.getSheet().createDrawingPatriarch().createCellComment(anchor); + comment.setString(helper.createRichTextString(text)); + cell.setCellComment(comment); + } + + // ==================== 样式 ==================== + + /** + * 创建细边框样式 + */ + public static CellStyle createBorderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + return style; + } + + /** + * 创建数据单元格样式:细边框、垂直居中,可指定锁定和填充色 + * + * @param workbook 工作簿 + * @param locked 是否锁定,配合 protectSheet 使用 + * @param fillColor 填充色,传 null 时不填充 + */ + public static CellStyle createDataStyle(Workbook workbook, boolean locked, IndexedColors fillColor) { + CellStyle style = createBorderStyle(workbook); + style.setLocked(locked); + if (fillColor != null) { + style.setFillForegroundColor(fillColor.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + } + style.setVerticalAlignment(VerticalAlignment.CENTER); + return style; + } + + /** + * 基于已有样式创建数字格式样式 + */ + public static CellStyle createNumberStyle(Workbook workbook, CellStyle base, String format) { + CellStyle style = workbook.createCellStyle(); + style.cloneStyleFrom(base); + style.setDataFormat(workbook.createDataFormat().getFormat(format)); + return style; + } + + /** + * 创建表头样式:细边框、指定填充色、加粗居中 + */ + public static CellStyle createHeaderStyle(Workbook workbook, IndexedColors fillColor) { + CellStyle style = createBorderStyle(workbook); + style.setFillForegroundColor(fillColor.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + return style; + } + + /** + * 创建标题样式:大号加粗居中 + */ + public static CellStyle createTitleStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 20); + style.setFont(font); + return style; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtilsTest.java b/yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtilsTest.java new file mode 100644 index 0000000..c6964d5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtilsTest.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.framework.excel.core.util; + +import cn.idev.excel.FastExcelFactory; +import cn.idev.excel.annotation.ExcelProperty; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link ExcelUtils} 的单元测试 + * + * @author 芋道源码 + */ +public class ExcelUtilsTest { + + @Test + public void testRead_maxRowCount() throws Exception { + // mock 数据 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + FastExcelFactory.write(outputStream, TestRow.class).sheet().doWrite(Arrays.asList( + new TestRow("员工一"), new TestRow("员工二"), new TestRow("员工三"))); + MockMultipartFile file = new MockMultipartFile( + "file", "employees.xlsx", null, outputStream.toByteArray()); + + // 调用 + List rows = ExcelUtils.read(file, TestRow.class, 2); + + // 断言 + assertEquals(2, rows.size()); + assertEquals("员工一", rows.get(0).getName()); + assertEquals("员工二", rows.get(1).getName()); + } + + public static class TestRow { + + @ExcelProperty("姓名") + private String name; + + public TestRow() { + } + + public TestRow(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java index 08bdf19..79e95fe 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -145,6 +145,18 @@ public interface BaseMapperX extends MPJBaseMapper { } /** + * 获得满足条件的第 1 条记录,并使用 FOR UPDATE 锁定。 + * + * 注意:需要在事务中调用,否则锁会立即释放。 + * + * @param queryWrapper 查询条件 + * @return 实体 + */ + default T selectFirstOneForUpdate(LambdaQueryWrapper queryWrapper) { + return selectOne(queryWrapper.last("LIMIT 1 FOR UPDATE")); + } + + /** * 获取满足条件的第 1 条记录 * * 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题 diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java index b766199..b5cbcd3 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import org.springframework.util.StringUtils; +import java.time.LocalDateTime; import java.util.Collection; /** @@ -107,6 +108,24 @@ public class LambdaQueryWrapperX extends LambdaQueryWrapper { return betweenIfPresent(column, val1, val2); } + /** + * 拼接两个闭区间的重叠条件 + * + *

记录区间与查询区间重叠的条件为:记录开始值小于等于查询结束值, + * 且记录结束值大于等于查询开始值。不能使用单字段 {@code BETWEEN} 代替, + * 否则会漏掉完整覆盖查询区间的记录。

+ * + * @param beginColumn 记录开始值字段 + * @param endColumn 记录结束值字段 + * @param times 查询闭区间 + * @return 查询条件 + */ + public LambdaQueryWrapperX betweenIfPresent( + SFunction beginColumn, SFunction endColumn, LocalDateTime[] times) { + return leIfPresent(beginColumn, ArrayUtils.get(times, 1)) + .geIfPresent(endColumn, ArrayUtils.get(times, 0)); + } + // ========== 重写父类方法,方便链式调用 ========== @Override diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java index bff0122..525d575 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java @@ -102,16 +102,9 @@ public class QueryWrapperX extends QueryWrapper { } public QueryWrapperX betweenIfPresent(String column, Object[] values) { - if (values != null && values.length != 0 && values[0] != null && values[1] != null) { - return (QueryWrapperX) super.between(column, values[0], values[1]); - } - if (values != null && values.length != 0 && values[0] != null) { - return (QueryWrapperX) ge(column, values[0]); - } - if (values != null && values.length != 0 && values[1] != null) { - return (QueryWrapperX) le(column, values[1]); - } - return this; + Object val1 = values != null && values.length > 0 ? values[0] : null; + Object val2 = values != null && values.length > 1 ? values[1] : null; + return betweenIfPresent(column, val1, val2); } // ========== 重写父类方法,方便链式调用 ========== diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java index ab4612c..1cdf7f1 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java @@ -24,6 +24,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * MyBatis 工具类 @@ -179,6 +181,28 @@ public class MyBatisUtils { return findInSet(columnName, paramIndex); } + /** + * 跨数据库的 find_in_set 实现,适用于同一字段匹配多个参数的场景 + * + * 每个参数生成一个 find_in_set 条件,并使用 OR 连接。 + * + * @param columnName 字段名称 + * @param values 参数集合 + * @return sql + */ + public static String findInSet(String columnName, Collection values) { + return findInSet(JdbcUtils.getDbType(), columnName, values); + } + + static String findInSet(DbType dbType, String columnName, Collection values) { + if (CollUtil.isEmpty(values)) { + throw new IllegalArgumentException("Values cannot be empty"); + } + return IntStream.range(0, values.size()) + .mapToObj(index -> findInSet(dbType, columnName, index)) + .collect(Collectors.joining(" OR ")); + } + private static String findInSet(String columnName, int paramIndex) { DbType dbType = JdbcUtils.getDbType(); return findInSet(dbType, columnName, paramIndex); diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperXTest.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperXTest.java new file mode 100644 index 0000000..e708e96 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperXTest.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.framework.mybatis.core.query; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import lombok.Data; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link LambdaQueryWrapperX} 的单元测试 + * + * @author 芋道源码 + */ +public class LambdaQueryWrapperXTest { + + @BeforeAll + public static void initTableInfo() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TestDO.class); + } + + @Test + public void testBetweenIfPresent_overlap() { + // 调用,并断言:空范围不追加条件 + assertEquals("", new LambdaQueryWrapperX() + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime, null) + .getSqlSegment()); + + // 调用,并断言:双边范围使用闭区间重叠条件 + LocalDateTime beginTime = LocalDateTime.of(2026, 8, 1, 0, 0); + LocalDateTime endTime = LocalDateTime.of(2026, 8, 31, 23, 59); + String sqlSegment = new LambdaQueryWrapperX() + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime, + new LocalDateTime[]{beginTime, endTime}) + .getSqlSegment(); + assertTrue(sqlSegment.contains("start_time <=")); + assertTrue(sqlSegment.contains("end_time >=")); + + // 调用,并断言:单边范围只追加对应的重叠边界 + sqlSegment = new LambdaQueryWrapperX() + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime, + new LocalDateTime[]{beginTime}) + .getSqlSegment(); + assertTrue(sqlSegment.contains("end_time >=")); + sqlSegment = new LambdaQueryWrapperX() + .betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime, + new LocalDateTime[]{null, endTime}) + .getSqlSegment(); + assertTrue(sqlSegment.contains("start_time <=")); + } + + @Data + @TableName("test_data") + private static class TestDO { + + private LocalDateTime startTime; + private LocalDateTime endTime; + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperXTest.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperXTest.java new file mode 100644 index 0000000..09a2674 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperXTest.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.mybatis.core.query; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link QueryWrapperX} 的单元测试 + * + * @author 芋道源码 + */ +public class QueryWrapperXTest { + + @Test + public void testBetweenIfPresent_array() { + // 调用,并断言:空范围不追加条件 + assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", (Object[]) null).getSqlSegment()); + assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", new Object[0]).getSqlSegment()); + + // 调用,并断言:单边范围安全降级为大于等于或小于等于 + assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1}) + .getSqlSegment().contains("create_time >=")); + assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{null, 2}) + .getSqlSegment().contains("create_time <=")); + + // 调用,并断言:双边范围使用左右闭合的 BETWEEN + assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1, 2}) + .getSqlSegment().contains("create_time BETWEEN")); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java index abe8891..b74558c 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java @@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.junit.jupiter.api.Test; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -129,6 +130,14 @@ public class MyBatisUtilsTest { } @Test + public void testFindInSet_multipleValues() { + assertEquals("FIND_IN_SET({0}, tag_ids) <> 0 OR FIND_IN_SET({1}, tag_ids) <> 0", + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Arrays.asList(1L, 2L))); + assertThrows(IllegalArgumentException.class, + () -> MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Collections.emptyList())); + } + + @Test public void testFindInSet_applyBindsValue() { // 准备参数 QueryWrapper query = new QueryWrapper<>(); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java index b8ccad6..7f1b9cf 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java @@ -19,6 +19,13 @@ public interface BpmProcessInstanceApi { */ String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO); - + /** + * 发起人取消运行中的流程实例(提供给内部) + * + * @param userId 发起人用户编号 + * @param processInstanceId 流程实例编号 + * @param reason 取消原因 + */ + void cancelProcessInstanceByStartUser(Long userId, String processInstanceId, String reason); } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java index 5ebb12f..5377314 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.bpm.api.task; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import jakarta.annotation.Resource; import jakarta.validation.Valid; @@ -25,4 +26,10 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi { return processInstanceService.createProcessInstance(userId, reqDTO); } + @Override + public void cancelProcessInstanceByStartUser(Long userId, String processInstanceId, String reason) { + processInstanceService.cancelProcessInstanceByStartUser(userId, + new BpmProcessInstanceCancelReqVO().setId(processInstanceId).setReason(reason)); + } + } diff --git a/yudao-module-fms/pom.xml b/yudao-module-fms/pom.xml new file mode 100644 index 0000000..dc91b66 --- /dev/null +++ b/yudao-module-fms/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + cn.iocoder.boot + yudao + ${revision} + + + yudao-module-fms + jar + + ${project.artifactId} + + fms 包下,财务管理(Financial Management System) + 例如说:凭证、账簿、报表、结账等等 + + + + + cn.iocoder.boot + yudao-module-system + ${revision} + + + cn.iocoder.boot + yudao-module-infra + ${revision} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-web + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + + com.googlecode.aviator + aviator + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingPeriodController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingPeriodController.java new file mode 100644 index 0000000..c93d72d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingPeriodController.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 结账期间") +@RestController +@RequestMapping("/fms/closing/period") +@Validated +public class FmsClosingPeriodController { + + @Resource + private FmsClosingPeriodService closingPeriodService; + + @GetMapping("/current-month") + @Operation(summary = "获得当前会计期间") + public CommonResult getCurrentMonth(@RequestParam("accountSetId") @NotNull Long accountSetId) { + return success(closingPeriodService.getCurrentMonth(accountSetId, getLoginUserId())); + } + + @GetMapping("/overview") + @Operation(summary = "获得结账概况") + @PreAuthorize("@ss.hasPermission('fms:closing:query')") + public CommonResult getClosingOverview(@Valid FmsClosingQueryReqVO queryReqVO) { + return success(closingPeriodService.getClosingOverview(queryReqVO, getLoginUserId())); + } + + @PutMapping("/close") + @Operation(summary = "结账") + @PreAuthorize("@ss.hasPermission('fms:closing:close')") + public CommonResult closePeriod(@Valid @RequestBody FmsClosingQueryReqVO queryReqVO) { + closingPeriodService.closePeriod(queryReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/cancel") + @Operation(summary = "反结账") + @PreAuthorize("@ss.hasPermission('fms:closing:cancel')") + public CommonResult cancelClosePeriod(@Valid FmsClosingQueryReqVO queryReqVO) { + closingPeriodService.cancelClosePeriod(queryReqVO, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingSchemeController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingSchemeController.java new file mode 100644 index 0000000..5e95e44 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingSchemeController.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossSettingsSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsSpecialClosingSettingsSaveReqVO; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 结账方案") +@RestController +@RequestMapping("/fms/closing/scheme") +@Validated +public class FmsClosingSchemeController { + + @Resource + private FmsClosingSchemeService closingSchemeService; + + @GetMapping("/list") + @Operation(summary = "获得结账方案列表") + @PreAuthorize("@ss.hasPermission('fms:closing:query')") + public CommonResult> getClosingSchemeList(@Valid FmsClosingQueryReqVO queryReqVO) { + return success(closingSchemeService.getClosingSchemeList(queryReqVO, getLoginUserId())); + } + + @PostMapping("/create") + @Operation(summary = "创建结账方案") + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult createClosingScheme(@Valid @RequestBody FmsClosingSchemeSaveReqVO createReqVO) { + return success(closingSchemeService.createClosingScheme(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改结账方案") + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult updateClosingScheme(@Valid @RequestBody FmsClosingSchemeSaveReqVO updateReqVO) { + closingSchemeService.updateClosingScheme(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-profit-loss-settings") + @Operation(summary = "保存结转损益设置") + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult saveProfitLossSettings(@Valid @RequestBody FmsProfitLossSettingsSaveReqVO saveReqVO) { + return success(closingSchemeService.saveProfitLossSettings(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update-special-settings") + @Operation(summary = "保存专用结转设置") + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult updateSpecialClosingSettings(@Valid @RequestBody FmsSpecialClosingSettingsSaveReqVO updateReqVO) { + closingSchemeService.updateSpecialClosingSettings(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除结账方案") + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult deleteClosingScheme(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + closingSchemeService.deleteClosingScheme(accountSetId, id, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingTemplateController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingTemplateController.java new file mode 100644 index 0000000..9fb6dc1 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingTemplateController.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateSaveReqVO; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 结账模板") +@RestController +@RequestMapping("/fms/closing/template") +@Validated +public class FmsClosingTemplateController { + + @Resource + private FmsClosingTemplateService closingTemplateService; + + @GetMapping("/list") + @Operation(summary = "获得结账模板列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:closing:query')") + public CommonResult> getClosingTemplateList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + return success(closingTemplateService.getClosingTemplateList(accountSetId, getLoginUserId())); + } + + @PostMapping("/create") + @Operation(summary = "创建结账模板") + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult createClosingTemplate( + @Valid @RequestBody FmsClosingTemplateSaveReqVO createReqVO) { + return success(closingTemplateService.createClosingTemplate(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新结账模板") + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult updateClosingTemplate( + @Valid @RequestBody FmsClosingTemplateSaveReqVO updateReqVO) { + closingTemplateService.updateClosingTemplate(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除结账模板") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "id", description = "结账模板编号", required = true, example = "1024") + }) + @PreAuthorize("@ss.hasPermission('fms:closing:update')") + public CommonResult deleteClosingTemplate(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + closingTemplateService.deleteClosingTemplate(accountSetId, id, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingVoucherController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingVoucherController.java new file mode 100644 index 0000000..4aa1d3b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/FmsClosingVoucherController.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingVoucherGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossGenerateReqVO; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 结转凭证") +@RestController +@RequestMapping("/fms/closing/voucher") +@Validated +public class FmsClosingVoucherController { + + @Resource + private FmsClosingVoucherService closingVoucherService; + + @PostMapping("/generate-profit-loss") + @Operation(summary = "生成结转损益凭证") + @PreAuthorize("@ss.hasPermission('fms:closing:profit-loss')") + public CommonResult generateProfitLossVoucher(@Valid @RequestBody FmsProfitLossGenerateReqVO generateReqVO) { + return success(closingVoucherService.generateProfitLossVoucher(generateReqVO, getLoginUserId())); + } + + @PostMapping("/generate-scheme") + @Operation(summary = "生成结账方案凭证") + @PreAuthorize("@ss.hasPermission('fms:closing:profit-loss')") + public CommonResult generateClosingSchemeVoucher(@Valid @RequestBody FmsClosingSchemeGenerateReqVO generateReqVO) { + return success(closingVoucherService.generateClosingSchemeVoucher(generateReqVO, getLoginUserId())); + } + + @PostMapping("/generate-list") + @Operation(summary = "批量生成结转凭证") + @PreAuthorize("@ss.hasPermission('fms:closing:profit-loss')") + public CommonResult> generateClosingVoucherList(@Valid @RequestBody FmsClosingVoucherGenerateReqVO generateReqVO) { + return success(closingVoucherService.generateClosingVoucherList(generateReqVO, getLoginUserId())); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingOverviewRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingOverviewRespVO.java new file mode 100644 index 0000000..e12f59f --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingOverviewRespVO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 结账概况 Response VO") +@Data +public class FmsClosingOverviewRespVO { + + @Schema(description = "会计期间", example = "2026-08") + private String month; + + @Schema(description = "是否已结账") + private Boolean closed; + + @Schema(description = "是否要求凭证审核") + private Boolean voucherReviewRequired; + + @Schema(description = "未审核凭证数量", example = "2") + private Long pendingVoucherCount; + + @Schema(description = "凭证数量", example = "12") + private Long voucherCount; + + @Schema(description = "损益类科目余额", example = "9100.00") + private BigDecimal profitLossBalance; + + @Schema(description = "资产负债表差额", example = "0.00") + private BigDecimal balanceSheetDifference; + + @Schema(description = "结转损益凭证编号", example = "1024") + private Long profitLossVoucherId; + + @Schema(description = "初始余额是否试算平衡") + private Boolean initialBalanceBalanced; + + @Schema(description = "凭证编号是否连续") + private Boolean voucherNumberContinuous; + + @Schema(description = "需要结转损益时是否已生成凭证") + private Boolean profitLossVoucherGenerated; + + @Schema(description = "利润表勾稽是否平衡") + private Boolean incomeStatementBalanced; + + @Schema(description = "利润表未纳入公式的科目数量", example = "0") + private Integer incomeStatementUnmappedSubjectCount; + + @Schema(description = "资产负债表损益是否已结转") + private Boolean balanceSheetProfitLossTransferred; + + @Schema(description = "资产负债表是否平衡") + private Boolean balanceSheetBalanced; + + @Schema(description = "资产负债表未纳入公式的科目数量", example = "0") + private Integer balanceSheetUnmappedSubjectCount; + + @Schema(description = "是否满足全部结账条件") + private Boolean canClose; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingQueryReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingQueryReqVO.java new file mode 100644 index 0000000..04e1521 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingQueryReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 结账查询 Request VO") +@Data +public class FmsClosingQueryReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "目标会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotBlank(message = "会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确") + private String month; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeGenerateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeGenerateReqVO.java new file mode 100644 index 0000000..14cfa82 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeGenerateReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 结账方案生成凭证 Request VO") +@Data +public class FmsClosingSchemeGenerateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "方案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "方案编号不能为空") + private Long id; + + @Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotBlank(message = "会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确") + private String month; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeRespVO.java new file mode 100644 index 0000000..30c3e58 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeRespVO.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 结账方案 Response VO") +@Data +public class FmsClosingSchemeRespVO { + + @Schema(description = "方案编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", example = "1") + private Long accountSetId; + + @Schema(description = "方案名称", example = "结转本月房租") + private String name; + + @Schema(description = "是否期末结转") + private Boolean periodEnd; + + @Schema(description = "来源科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "取数规则", example = "0") + private Integer formulaRule; + + @Schema(description = "取数时间类型", example = "1") + private Integer timeType; + + @Schema(description = "凭证字编号", example = "11") + private Long voucherWordId; + + @Schema(description = "凭证摘要", example = "结转损益") + private String digest; + + @Schema(description = "结转凭证类型", example = "2") + private Integer voucherType; + + @Schema(description = "以前年度损益调整科目编号", example = "1024") + private Long priorYearAdjustmentSubjectId; + + @Schema(description = "以前年度损益调整结转科目编号", example = "1025") + private Long adjustmentClosingSubjectId; + + @Schema(description = "其他损益结转科目编号", example = "1026") + private Long otherClosingSubjectId; + + @Schema(description = "是否按余额反向结转") + private Boolean reverseBalance; + + @Schema(description = "结转日期", example = "31") + private Integer closingDay; + + @Schema(description = "类型", example = "1") + private Integer type; + + @Schema(description = "结转科目规则数组") + private List subjects; + + @Schema(description = "待结转金额", example = "12000.00") + private BigDecimal balance; + + @Schema(description = "当前期间已生成凭证编号数组") + private List voucherIds; + + @Schema(description = "管理后台 - FMS 结账方案科目规则") + @Data + public static class SubjectRule { + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目编码快照", example = "6602") + private String subjectCode; + + @Schema(description = "摘要", example = "结转本月房租") + private String digest; + + @Schema(description = "借贷方向", example = "1") + private Integer direction; + + @Schema(description = "金额比例", example = "100") + private BigDecimal amountRatio; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeSaveReqVO.java new file mode 100644 index 0000000..80d6b9f --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingSchemeSaveReqVO.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 结账方案保存 Request VO") +@Data +public class FmsClosingSchemeSaveReqVO { + + @Schema(description = "方案编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "方案名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "结转本月房租") + @NotBlank(message = "方案名称不能为空") + @Size(max = 255, message = "方案名称长度不能超过 255 个字符") + private String name; + + @Schema(description = "是否期末结转", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "是否期末结转不能为空") + private Boolean periodEnd; + + @Schema(description = "来源科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "来源科目不能为空") + private Long subjectId; + + @Schema(description = "取数规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "取数规则不能为空") + @InEnum(FmsFormulaRuleEnum.class) + private Integer formulaRule; + + @Schema(description = "取数时间类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "取数时间类型不能为空") + @InEnum(FmsClosingTimeTypeEnum.class) + private Integer timeType; + + @Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + @NotNull(message = "凭证字不能为空") + private Long voucherWordId; + + @Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "结转科目规则不能为空") + @Size(min = 2, message = "结转科目规则至少需要两条") + private List subjects; + + @Schema(description = "管理后台 - FMS 结账方案科目规则") + @Data + public static class SubjectRule { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "科目不能为空") + private Long subjectId; + + @Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "结转本月房租") + @NotBlank(message = "摘要不能为空") + @Size(max = 500, message = "摘要长度不能超过 500 个字符") + private String digest; + + @Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "借贷方向不能为空") + @InEnum(FmsDebitCreditDirectionEnum.class) + private Integer direction; + + @Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "金额比例不能为空") + @DecimalMin(value = "0", inclusive = false, message = "金额比例必须大于 0") + @DecimalMax(value = "100", message = "金额比例不能超过 100") + private BigDecimal amountRatio; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingTemplateRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingTemplateRespVO.java new file mode 100644 index 0000000..0789aad --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingTemplateRespVO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - FMS 结账模板 Response VO") +@Data +public class FmsClosingTemplateRespVO { + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountSetId; + + @Schema(description = "系统预置编码", example = "daily-travel-reimbursement") + private String presetCode; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费") + private String name; + + @Schema(description = "模板分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer category; + + @Schema(description = "是否期末结转", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean periodEnd; + + @Schema(description = "来源科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "取数规则", example = "0") + private Integer formulaRule; + + @Schema(description = "取数时间类型", example = "1") + private Integer timeType; + + @Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List subjects; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "管理后台 - FMS 结账模板科目规则") + @Data + public static class SubjectRule { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "560107") + private String subjectCode; + + @Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费") + private String digest; + + @Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer direction; + + @Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private BigDecimal amountRatio; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingTemplateSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingTemplateSaveReqVO.java new file mode 100644 index 0000000..af914a3 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingTemplateSaveReqVO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTemplateCategoryEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 结账模板保存 Request VO") +@Data +public class FmsClosingTemplateSaveReqVO { + + @Schema(description = "模板编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费") + @NotBlank(message = "模板名称不能为空") + @Size(max = 255, message = "模板名称长度不能超过 255 个字符") + private String name; + + @Schema(description = "模板分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模板分类不能为空") + @InEnum(FmsClosingTemplateCategoryEnum.class) + private Integer category; + + @Schema(description = "是否期末结转", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "是否期末结转不能为空") + private Boolean periodEnd; + + @Schema(description = "来源科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "取数规则", example = "0") + @InEnum(FmsFormulaRuleEnum.class) + private Integer formulaRule; + + @Schema(description = "取数时间类型", example = "1") + @InEnum(FmsClosingTimeTypeEnum.class) + private Integer timeType; + + @Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "结转科目规则不能为空") + @Size(min = 2, message = "结转科目规则至少需要两条") + private List subjects; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "管理后台 - FMS 结账模板科目规则") + @Data + public static class SubjectRule { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "科目不能为空") + private Long subjectId; + + @Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费") + @NotBlank(message = "摘要不能为空") + @Size(max = 500, message = "摘要长度不能超过 500 个字符") + private String digest; + + @Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "借贷方向不能为空") + @InEnum(FmsDebitCreditDirectionEnum.class) + private Integer direction; + + @Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "金额比例不能为空") + @DecimalMin(value = "0", inclusive = false, message = "金额比例必须大于 0") + @DecimalMax(value = "100", message = "金额比例不能超过 100") + private BigDecimal amountRatio; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingVoucherGenerateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingVoucherGenerateReqVO.java new file mode 100644 index 0000000..36970d8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsClosingVoucherGenerateReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 批量生成结转凭证 Request VO") +@Data +public class FmsClosingVoucherGenerateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "方案编号数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "方案编号不能为空") + private List<@NotNull(message = "方案编号不能为空") Long> ids; + + @Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotBlank(message = "会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确") + private String month; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsProfitLossGenerateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsProfitLossGenerateReqVO.java new file mode 100644 index 0000000..d48e181 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsProfitLossGenerateReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 生成结转损益凭证 Request VO") +@Data +public class FmsProfitLossGenerateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotBlank(message = "会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确") + private String month; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsProfitLossSettingsSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsProfitLossSettingsSaveReqVO.java new file mode 100644 index 0000000..85e5908 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsProfitLossSettingsSaveReqVO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingVoucherTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 结转损益设置保存 Request VO") +@Data +public class FmsProfitLossSettingsSaveReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + @NotNull(message = "凭证字不能为空") + private Long voucherWordId; + + @Schema(description = "凭证摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "结转损益") + @NotBlank(message = "凭证摘要不能为空") + @Size(max = 500, message = "凭证摘要长度不能超过 500 个字符") + private String digest; + + @Schema(description = "结转凭证类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "结转凭证类型不能为空") + @InEnum(FmsClosingVoucherTypeEnum.class) + private Integer voucherType; + + @Schema(description = "以前年度损益调整科目编号", requiredMode = Schema.RequiredMode.REQUIRED, + example = "1024") + @NotNull(message = "以前年度损益调整科目不能为空") + private Long priorYearAdjustmentSubjectId; + + @Schema(description = "以前年度损益调整结转科目编号", requiredMode = Schema.RequiredMode.REQUIRED, + example = "1025") + @NotNull(message = "以前年度损益调整结转科目不能为空") + private Long adjustmentClosingSubjectId; + + @Schema(description = "其他损益结转科目编号", requiredMode = Schema.RequiredMode.REQUIRED, + example = "1026") + @NotNull(message = "其他损益结转科目不能为空") + private Long otherClosingSubjectId; + + @Schema(description = "是否按余额反向结转", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结转方式不能为空") + private Boolean reverseBalance; + + @Schema(description = "结转日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") + @NotNull(message = "结转日期不能为空") + @Min(value = 1, message = "结转日期不能小于 1") + @Max(value = 31, message = "结转日期不能大于 31") + private Integer closingDay; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsSpecialClosingSettingsSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsSpecialClosingSettingsSaveReqVO.java new file mode 100644 index 0000000..5e19ab5 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/closing/vo/FmsSpecialClosingSettingsSaveReqVO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.fms.controller.admin.closing.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 专用结转设置保存 Request VO") +@Data +public class FmsSpecialClosingSettingsSaveReqVO { + + @Schema(description = "方案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "方案编号不能为空") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + @NotNull(message = "凭证字不能为空") + private Long voucherWordId; + + @Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "结转科目规则不能为空") + @Size(min = 2, message = "结转科目规则至少需要两条") + private List subjects; + + @Schema(description = "管理后台 - FMS 专用结转科目规则") + @Data + public static class SubjectRule { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "科目不能为空") + private Long subjectId; + + @Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "计提所得税") + @NotBlank(message = "摘要不能为空") + @Size(max = 500, message = "摘要长度不能超过 500 个字符") + private String digest; + + @Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "借贷方向不能为空") + @InEnum(FmsDebitCreditDirectionEnum.class) + private Integer direction; + + @Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "25") + @NotNull(message = "金额比例不能为空") + @DecimalMin(value = "0", inclusive = false, message = "金额比例必须大于 0") + @DecimalMax(value = "100", message = "金额比例不能超过 100") + private BigDecimal amountRatio; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAccountSetController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAccountSetController.java new file mode 100644 index 0000000..d139d75 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAccountSetController.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 账套") +@RestController +@RequestMapping("/fms/config/account-set") +@Validated +public class FmsAccountSetController { + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsAccountUserService accountUserService; + + @PostMapping("/create") + @Operation(summary = "创建账套") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:create')") + public CommonResult createAccountSet(@Valid @RequestBody FmsAccountSetSaveReqVO createReqVO) { + return success(accountSetService.createAccountSet(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新账套") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:update')") + public CommonResult updateAccountSet(@Valid @RequestBody FmsAccountSetSaveReqVO updateReqVO) { + accountSetService.updateAccountSet(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/initialize") + @Operation(summary = "初始化账套") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:initialize')") + public CommonResult initializeAccountSet( + @Valid @RequestBody FmsAccountSetInitializeReqVO initializeReqVO) { + accountSetService.initializeAccountSet(initializeReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得账套") + @Parameter(name = "id", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:query')") + public CommonResult getAccountSet(@RequestParam("id") Long id) { + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(id, getLoginUserId()); + FmsAccountUserDO accountUser = accountUserService.getAccountUser(id, getLoginUserId()); + return success(buildAccountSetRespVO(accountSet, accountUser)); + } + + @GetMapping("/list") + @Operation(summary = "获得当前用户账套列表") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:query')") + public CommonResult> getAccountSetList() { + List accountUsers = accountUserService.getAccountUserList(getLoginUserId()); + Map accountSetMap = accountSetService.getAccountSetMap( + convertSet(accountUsers, FmsAccountUserDO::getAccountSetId)); + return success(buildAccountSetRespVOList(accountSetMap, accountUsers)); + } + + // ==================== 拼接 VO ==================== + + private FmsAccountSetRespVO buildAccountSetRespVO( + FmsAccountSetDO accountSet, FmsAccountUserDO accountUser) { + return BeanUtils.toBean(accountSet, FmsAccountSetRespVO.class) + .setDefaultStatus(accountUser.getDefaultStatus()).setFounder(accountUser.getFounder()) + .setLevel(accountUser.getLevel()); + } + + private List buildAccountSetRespVOList( + Map accountSetMap, List accountUsers) { + return convertList(accountUsers, accountUser -> buildAccountSetRespVO( + accountSetMap.get(accountUser.getAccountSetId()), accountUser)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAccountUserController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAccountUserController.java new file mode 100644 index 0000000..d055ab7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAccountUserController.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser.FmsAccountUserRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser.FmsAccountUserUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountUserService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 账套用户") +@RestController +@RequestMapping("/fms/config/account-user") +@Validated +public class FmsAccountUserController { + + @Resource + private FmsAccountUserService accountUserService; + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @GetMapping("/list") + @Operation(summary = "获得账套用户列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:authorize')") + public CommonResult> getAccountUserList( + @RequestParam("accountSetId") Long accountSetId) { + List accountUsers = accountUserService.getAccountUserList( + accountSetId, getLoginUserId()); + return success(buildAccountUserRespVOList(accountUsers)); + } + + @PutMapping("/update") + @Operation(summary = "更新账套用户列表") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:authorize')") + public CommonResult updateAccountUserList( + @Valid @RequestBody FmsAccountUserUpdateReqVO updateReqVO) { + accountUserService.updateAccountUserList(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-default-status") + @Operation(summary = "更新默认账套状态") + @PreAuthorize("@ss.hasPermission('fms:config:account-set:query')") + public CommonResult updateAccountSetDefaultStatus(@RequestParam("accountSetId") Long accountSetId) { + accountUserService.updateAccountSetDefaultStatus(accountSetId, getLoginUserId()); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private List buildAccountUserRespVOList(List accountUsers) { + Map userMap = adminUserApi.getUserMap( + convertSet(accountUsers, FmsAccountUserDO::getUserId)); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + return convertList(accountUsers, accountUser -> { + FmsAccountUserRespVO respVO = BeanUtils.toBean(accountUser, FmsAccountUserRespVO.class); + findAndThen(userMap, accountUser.getUserId(), user -> { + respVO.setNickname(user.getNickname()).setMobile(user.getMobile()) + .setEmail(user.getEmail()).setStatus(user.getStatus()); + findAndThen(deptMap, user.getDeptId(), dept -> respVO.setDeptName(dept.getName())); + }); + return respVO; + }); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAuxiliaryItemController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAuxiliaryItemController.java new file mode 100644 index 0000000..9abc515 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAuxiliaryItemController.java @@ -0,0 +1,228 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemStatusReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryItemService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 辅助核算项目") +@RestController +@RequestMapping("/fms/config/auxiliary-item") +@Validated +public class FmsAuxiliaryItemController { + + @Resource + private FmsAuxiliaryItemService auxiliaryItemService; + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + + @PostMapping("/create") + @Operation(summary = "创建辅助核算项目") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:create')") + public CommonResult createAuxiliaryItem(@Valid @RequestBody FmsAuxiliaryItemSaveReqVO createReqVO) { + return success(auxiliaryItemService.createAuxiliaryItem(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新辅助核算项目") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:update')") + public CommonResult updateAuxiliaryItem( + @Valid @RequestBody FmsAuxiliaryItemSaveReqVO updateReqVO) { + auxiliaryItemService.updateAuxiliaryItem(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "删除辅助核算项目") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "ids", description = "辅助核算项目编号数组", required = true, example = "[1024]") + }) + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:delete')") + public CommonResult deleteAuxiliaryItemList( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("ids") @NotEmpty List ids) { + auxiliaryItemService.deleteAuxiliaryItemList(accountSetId, ids, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新辅助核算项目状态") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:update')") + public CommonResult updateAuxiliaryItemStatus( + @Valid @RequestBody FmsAuxiliaryItemStatusReqVO reqVO) { + auxiliaryItemService.updateAuxiliaryItemStatus( + reqVO.getAccountSetId(), reqVO.getId(), reqVO.getStatus(), getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得辅助核算项目") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "id", description = "辅助核算项目编号", required = true, example = "1024") + }) + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:query')") + public CommonResult getAuxiliaryItem(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + FmsAuxiliaryItemDO item = auxiliaryItemService.getAuxiliaryItem(accountSetId, id, getLoginUserId()); + return success(BeanUtils.toBean(item, FmsAuxiliaryItemRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得辅助核算项目分页") + @PreAuthorize("@ss.hasAnyPermissions('fms:config:auxiliary:query', 'fms:voucher:create', " + + "'fms:voucher:update')") + public CommonResult> getAuxiliaryItemPage( + @Valid FmsAuxiliaryItemPageReqVO pageReqVO) { + PageResult pageResult = auxiliaryItemService + .getAuxiliaryItemPage(pageReqVO, getLoginUserId()); + return success(BeanUtils.toBean(pageResult, FmsAuxiliaryItemRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得辅助核算项目精简列表", description = "只包含启用的项目,主要用于前端下拉选项") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "auxiliaryTypeId", description = "辅助核算类别编号", required = true, + example = "1024") + }) + public CommonResult> getAuxiliaryItemSimpleList( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("auxiliaryTypeId") @NotNull Long auxiliaryTypeId) { + List list = auxiliaryItemService + .getAuxiliaryItemListByAccountSetIdAndAuxiliaryTypeIdAndStatus( + accountSetId, auxiliaryTypeId, CommonStatusEnum.ENABLE.getStatus(), getLoginUserId()); + return success(convertList(list, item -> new FmsAuxiliaryItemRespVO() + .setId(item.getId()).setCode(item.getCode()).setName(item.getName()))); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出辅助核算项目") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportAuxiliaryItemExcel( + @Valid FmsAuxiliaryItemPageReqVO pageReqVO, HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = auxiliaryItemService + .getAuxiliaryItemPage(pageReqVO, getLoginUserId()).getList(); + FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeService.getAuxiliaryType( + pageReqVO.getAccountSetId(), pageReqVO.getAuxiliaryTypeId(), getLoginUserId()); + ExcelUtils.write(response, auxiliaryType.getName() + ".xlsx", "辅助核算项目", + buildAuxiliaryItemExcelHead(auxiliaryType.getType()), buildAuxiliaryItemExcelData(list, + auxiliaryType.getType())); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得辅助核算项目导入模板") + @Parameter(name = "type", description = "辅助核算类别类型", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:import')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getAuxiliaryItemImportTemplate( + @RequestParam("type") @InEnum(FmsAuxiliaryTypeEnum.class) Integer type, + HttpServletResponse response) throws IOException { + FmsAuxiliaryItemImportExcelVO example = new FmsAuxiliaryItemImportExcelVO() + .setCode("001").setName("示例项目").setRemark("示例备注"); + if (FmsAuxiliaryTypeEnum.INVENTORY.getType().equals(type)) { + example.setSpecification("标准版").setUnit("个"); + } + ExcelUtils.write(response, "辅助核算项目导入模板.xlsx", "辅助核算项目", + FmsAuxiliaryItemImportExcelVO.class, Collections.singletonList(example)); + } + + @PostMapping("/import") + @Operation(summary = "导入辅助核算项目") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true), + @Parameter(name = "auxiliaryTypeId", description = "辅助核算类别编号", required = true), + @Parameter(name = "file", description = "Excel 文件", required = true) + }) + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:import')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult importAuxiliaryItem( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("auxiliaryTypeId") @NotNull Long auxiliaryTypeId, + @RequestParam("file") MultipartFile file) throws Exception { + List importItems = ExcelUtils.read( + file, FmsAuxiliaryItemImportExcelVO.class); + return success(auxiliaryItemService.importAuxiliaryItemList( + accountSetId, auxiliaryTypeId, importItems, getLoginUserId())); + } + + /** 构建辅助核算项目导出的动态表头 */ + private List> buildAuxiliaryItemExcelHead(Integer type) { + if (FmsAuxiliaryTypeEnum.CUSTOMER.getType().equals(type) + || FmsAuxiliaryTypeEnum.SUPPLIER.getType().equals(type)) { + return Arrays.asList(Collections.singletonList("编码"), Collections.singletonList("名称"), + Collections.singletonList("备注")); + } + if (FmsAuxiliaryTypeEnum.INVENTORY.getType().equals(type)) { + return Arrays.asList(Collections.singletonList("编码"), Collections.singletonList("名称"), + Collections.singletonList("规格"), Collections.singletonList("单位")); + } + return Arrays.asList(Collections.singletonList("编码"), Collections.singletonList("名称")); + } + + /** 构建辅助核算项目导出的动态数据 */ + private List> buildAuxiliaryItemExcelData(List items, Integer type) { + List> rows = new ArrayList<>(items.size()); + for (FmsAuxiliaryItemDO item : items) { + if (FmsAuxiliaryTypeEnum.CUSTOMER.getType().equals(type) + || FmsAuxiliaryTypeEnum.SUPPLIER.getType().equals(type)) { + rows.add(Arrays.asList(item.getCode(), item.getName(), item.getRemark())); + } else if (FmsAuxiliaryTypeEnum.INVENTORY.getType().equals(type)) { + rows.add(Arrays.asList(item.getCode(), item.getName(), item.getSpecification(), item.getUnit())); + } else { + rows.add(Arrays.asList(item.getCode(), item.getName())); + } + } + return rows; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAuxiliaryTypeController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAuxiliaryTypeController.java new file mode 100644 index 0000000..b44f548 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsAuxiliaryTypeController.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype.FmsAuxiliaryTypeRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype.FmsAuxiliaryTypeSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 辅助核算类别") +@RestController +@RequestMapping("/fms/config/auxiliary-type") +@Validated +public class FmsAuxiliaryTypeController { + + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + + @PostMapping("/create") + @Operation(summary = "创建辅助核算类别") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:create')") + public CommonResult createAuxiliaryType(@Valid @RequestBody FmsAuxiliaryTypeSaveReqVO createReqVO) { + return success(auxiliaryTypeService.createAuxiliaryType(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新辅助核算类别") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:update')") + public CommonResult updateAuxiliaryType(@Valid @RequestBody FmsAuxiliaryTypeSaveReqVO updateReqVO) { + auxiliaryTypeService.updateAuxiliaryType(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除辅助核算类别") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "id", description = "辅助核算类别编号", required = true, example = "1024") + }) + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:delete')") + public CommonResult deleteAuxiliaryType( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + auxiliaryTypeService.deleteAuxiliaryType(accountSetId, id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得辅助核算类别列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:auxiliary:query')") + public CommonResult> getAuxiliaryTypeList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + return success(BeanUtils.toBean(auxiliaryTypeService.getAuxiliaryTypeList( + accountSetId, getLoginUserId()), FmsAuxiliaryTypeRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得辅助核算类别精简列表", description = "主要用于前端的下拉选项") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + public CommonResult> getAuxiliaryTypeSimpleList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + List list = auxiliaryTypeService.getAuxiliaryTypeList( + accountSetId, getLoginUserId()); + return success(convertList(list, auxiliaryType -> new FmsAuxiliaryTypeRespVO() + .setId(auxiliaryType.getId()).setName(auxiliaryType.getName()).setType(auxiliaryType.getType()))); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsCurrencyController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsCurrencyController.java new file mode 100644 index 0000000..0f583b1 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsCurrencyController.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency.FmsCurrencyRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency.FmsCurrencySaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.service.config.FmsCurrencyService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 币别") +@RestController +@RequestMapping("/fms/config/currency") +@Validated +public class FmsCurrencyController { + + @Resource + private FmsCurrencyService currencyService; + + @PostMapping("/create") + @Operation(summary = "创建币别") + @PreAuthorize("@ss.hasPermission('fms:config:currency:create')") + public CommonResult createCurrency(@Valid @RequestBody FmsCurrencySaveReqVO createReqVO) { + return success(currencyService.createCurrency(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新币别") + @PreAuthorize("@ss.hasPermission('fms:config:currency:update')") + public CommonResult updateCurrency(@Valid @RequestBody FmsCurrencySaveReqVO updateReqVO) { + currencyService.updateCurrency(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除币别") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "id", description = "币别编号", required = true, example = "1024") + }) + @PreAuthorize("@ss.hasPermission('fms:config:currency:delete')") + public CommonResult deleteCurrency( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + currencyService.deleteCurrency(accountSetId, id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得币别列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:currency:query')") + public CommonResult> getCurrencyList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + List list = currencyService.getCurrencyList(accountSetId, getLoginUserId()); + return success(BeanUtils.toBean(list, FmsCurrencyRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得币别精简列表", description = "主要用于前端的下拉选项") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + public CommonResult> getCurrencySimpleList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + List list = currencyService.getCurrencyList(accountSetId, getLoginUserId()); + return success(convertList(list, currency -> new FmsCurrencyRespVO() + .setId(currency.getId()).setCode(currency.getCode()).setName(currency.getName()) + .setExchangeRate(currency.getExchangeRate()).setStandard(currency.getStandard()))); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsDigestController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsDigestController.java new file mode 100644 index 0000000..3eafb82 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsDigestController.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest.FmsDigestRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest.FmsDigestSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsDigestDO; +import cn.iocoder.yudao.module.fms.service.config.FmsDigestService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * FMS 常用摘要 Controller + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - FMS 常用摘要") +@RestController +@RequestMapping("/fms/config/digest") +@Validated +public class FmsDigestController { + + @Resource + private FmsDigestService digestService; + + @PostMapping("/create") + @Operation(summary = "创建常用摘要") + @PreAuthorize("@ss.hasPermission('fms:config:digest:create')") + public CommonResult createDigest(@Valid @RequestBody FmsDigestSaveReqVO createReqVO) { + return success(digestService.createDigest(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改常用摘要") + @PreAuthorize("@ss.hasPermission('fms:config:digest:update')") + public CommonResult updateDigest(@Valid @RequestBody FmsDigestSaveReqVO updateReqVO) { + digestService.updateDigest(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除常用摘要") + @Parameter(name = "id", description = "常用摘要编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:digest:delete')") + public CommonResult deleteDigest(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + digestService.deleteDigest(accountSetId, id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得常用摘要管理列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:digest:query')") + public CommonResult> getDigestList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + List list = digestService.getDigestList(accountSetId, getLoginUserId()); + return success(BeanUtils.toBean(list, FmsDigestRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得常用摘要精简列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + public CommonResult> getDigestSimpleList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + List list = digestService.getDigestList(accountSetId, getLoginUserId()); + return success(convertList(list, digest -> new FmsDigestRespVO() + .setId(digest.getId()).setAccountSetId(digest.getAccountSetId()).setContent(digest.getContent()))); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsFinanceIndicatorController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsFinanceIndicatorController.java new file mode 100644 index 0000000..5ea15d5 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsFinanceIndicatorController.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator.FmsFinanceIndicatorRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator.FmsFinanceIndicatorSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceIndicatorDO; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceIndicatorService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 财务指标") +@RestController +@RequestMapping("/fms/config/finance-indicator") +@Validated +public class FmsFinanceIndicatorController { + + @Resource + private FmsFinanceIndicatorService financeIndicatorService; + + @PostMapping("/create") + @Operation(summary = "创建首页财务指标") + @PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:create')") + public CommonResult create(@Valid @RequestBody FmsFinanceIndicatorSaveReqVO reqVO) { + return success(financeIndicatorService.createFinanceIndicator(reqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改首页财务指标") + @PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:update')") + public CommonResult update(@Valid @RequestBody FmsFinanceIndicatorSaveReqVO reqVO) { + financeIndicatorService.updateFinanceIndicator(reqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除首页财务指标") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @Parameter(name = "id", description = "指标编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:delete')") + public CommonResult delete(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + financeIndicatorService.deleteFinanceIndicator(accountSetId, id, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得首页财务指标") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @Parameter(name = "id", description = "指标编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:query')") + public CommonResult get(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + FmsFinanceIndicatorDO indicator = financeIndicatorService.getFinanceIndicator(accountSetId, id, getLoginUserId()); + return success(BeanUtils.toBean(indicator, FmsFinanceIndicatorRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得首页财务指标列表") + @PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:query')") + public CommonResult> getList(@RequestParam("accountSetId") @NotNull Long accountSetId) { + List list = financeIndicatorService.getFinanceIndicatorList(accountSetId, + getLoginUserId()); + return success(BeanUtils.toBean(list, FmsFinanceIndicatorRespVO.class)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsFinanceParameterController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsFinanceParameterController.java new file mode 100644 index 0000000..931b949 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsFinanceParameterController.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter.FmsFinanceParameterRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter.FmsFinanceParameterUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 财务参数") +@RestController +@RequestMapping("/fms/config/finance-parameter") +@Validated +public class FmsFinanceParameterController { + + @Resource + private FmsFinanceParameterService financeParameterService; + + @GetMapping("/get") + @Operation(summary = "获得财务参数") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:finance-parameter:query')") + public CommonResult getFinanceParameter( + @RequestParam("accountSetId") Long accountSetId) { + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter( + accountSetId, getLoginUserId()); + return success(BeanUtils.toBean(financeParameter, FmsFinanceParameterRespVO.class)); + } + + @PutMapping("/update") + @Operation(summary = "更新财务参数") + @PreAuthorize("@ss.hasPermission('fms:config:finance-parameter:update')") + public CommonResult updateFinanceParameter( + @Valid @RequestBody FmsFinanceParameterUpdateReqVO updateReqVO) { + financeParameterService.updateFinanceParameter(updateReqVO, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsInitialBalanceController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsInitialBalanceController.java new file mode 100644 index 0000000..16498bc --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsInitialBalanceController.java @@ -0,0 +1,136 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceExcelVO; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_IMPORT_TEMPLATE_INVALID; + +@Tag(name = "管理后台 - FMS 初始余额") +@RestController +@RequestMapping("/fms/config/initial-balance") +@Validated +public class FmsInitialBalanceController { + + @Resource + private FmsInitialBalanceService initialBalanceService; + @Resource + private FmsAccountSetService accountSetService; + + @GetMapping("/list") + @Operation(summary = "获得初始余额列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @Parameter(name = "subjectType", description = "科目类型", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('fms:config:initial-balance:query')") + public CommonResult> getInitialBalanceList( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("subjectType") @NotNull Integer subjectType) { + return success(initialBalanceService.getInitialBalanceList(accountSetId, subjectType, getLoginUserId())); + } + + @PutMapping("/save") + @Operation(summary = "保存初始余额") + @PreAuthorize("@ss.hasPermission('fms:config:initial-balance:update')") + public CommonResult saveInitialBalance(@Valid @RequestBody FmsInitialBalanceSaveReqVO saveReqVO) { + initialBalanceService.saveInitialBalance(saveReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/trial-balance") + @Operation(summary = "获得试算平衡结果") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:initial-balance:query')") + public CommonResult getTrialBalance( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + return success(initialBalanceService.getTrialBalance(accountSetId, getLoginUserId())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出初始余额") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:initial-balance:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportInitialBalance( + @RequestParam("accountSetId") @NotNull Long accountSetId, + HttpServletResponse response) throws IOException { + // 1. 读取初始余额列表,并展开为 Excel 行 + List rows = FmsInitialBalanceExcelHelper.toRows( + initialBalanceService.getInitialBalanceList(accountSetId, null, getLoginUserId()), true); + // 2. 输出 Excel + ExcelUtils.write(response, "财务初始余额.xlsx", "财务初始余额", + FmsInitialBalanceExcelVO.class, rows); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得初始余额导入模板") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:initial-balance:import')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getInitialBalanceImportTemplate(@RequestParam("accountSetId") @NotNull Long accountSetId, + HttpServletResponse response) + throws IOException { + // 1. 读取初始余额列表,并展开为 Excel 行 + Long userId = getLoginUserId(); + List initialBalances = initialBalanceService.getInitialBalanceList(accountSetId, null, userId); + List rows = FmsInitialBalanceExcelHelper.toRows(initialBalances, false); + // 2. 生成导入模板,一月启用时只需要录入期初余额 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(accountSetId, userId); + byte[] content = FmsInitialBalanceExcelHelper.writeTemplate( + rows, LocalDateTimeUtils.isJanuary(accountSet.getStartTime())); + // 3. 写入响应 + ServletUtils.writeAttachment(response, "财务初始余额导入模板.xlsx", content); + } + + @PostMapping("/import") + @Operation(summary = "导入初始余额") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:initial-balance:import')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult importInitialBalance( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("file") MultipartFile file) throws IOException { + // 1. 读取 Excel + List rows; + try { + rows = FmsInitialBalanceExcelHelper.read(file); + } catch (IllegalArgumentException | IOException ex) { + throw exception(INITIAL_BALANCE_IMPORT_TEMPLATE_INVALID); + } + // 2. 导入数据 + return success(initialBalanceService.importInitialBalance(accountSetId, rows, getLoginUserId())); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsInitialBalanceExcelHelper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsInitialBalanceExcelHelper.java new file mode 100644 index 0000000..8b0d445 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsInitialBalanceExcelHelper.java @@ -0,0 +1,343 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceRespVO; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceExcelVO; +import lombok.experimental.UtilityClass; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.number.NumberUtils.zeroIfNull; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.createDataStyle; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.createHeaderStyle; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.createNumberStyle; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.getCellText; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.getDecimal; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.isEmptyRow; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.mergeHeader; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.writeFormula; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.writeNumber; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.writeText; + +/** + * FMS 初始余额 Excel 工具 + * + * @author 芋道源码 + */ +@UtilityClass +public final class FmsInitialBalanceExcelHelper { + + private static final String TEMPLATE_SHEET_NAME = "初始余额模板"; + + /** + * 展开初始余额平铺列表为 Excel 行 + * + * 父级科目仅在 includeParent 时输出行;启用辅助核算且存在明细的科目按明细逐行输出 + */ + public static List toRows(List results, + boolean includeParent) { + Set parentIds = convertSet(results, FmsInitialBalanceRespVO::getParentId); + List rows = new ArrayList<>(); + results.forEach(result -> { + if (parentIds.contains(result.getSubjectId())) { + if (includeParent) { + rows.add(buildRow(result, null)); + } + return; + } + if (Boolean.FALSE.equals(result.getAuxiliaryAccounting()) + || CollUtil.isEmpty(result.getAssistBalances())) { + rows.add(buildRow(result, null)); + return; + } + result.getAssistBalances().forEach(assist -> rows.add(buildRow(result, assist))); + }); + return rows; + } + + /** + * 构建单个科目或辅助核算明细对应的 Excel 行 + */ + private static FmsInitialBalanceExcelVO buildRow(FmsInitialBalanceRespVO result, + FmsInitialBalanceRespVO.AssistBalance assist) { + FmsInitialBalanceExcelVO row = new FmsInitialBalanceExcelVO() + .setSubjectCode(result.getSubjectCode()).setSubjectName(result.getSubjectName()) + .setDirectionName(FmsDebitCreditDirectionEnum.valueOf(result.getBalanceDirection()).getName()) + .setSubjectType(result.getType()).setQuantityAccounting(result.getQuantityAccounting()) + .setAuxiliaryAccounting(result.getAuxiliaryAccounting()); + if (assist == null) { + copyAmounts(result, row); + } else { + // 辅助核算项目拼接为“类别:名称/类别:名称”,与导入解析的格式对应 + Map auxiliaryTypeNameMap = convertMap(result.getAuxiliaryConfigs(), + FmsInitialBalanceRespVO.AuxiliaryConfig::getAuxiliaryTypeId, + FmsInitialBalanceRespVO.AuxiliaryConfig::getName); + row.setAuxiliaryItems(String.join("/", convertList(assist.getAuxiliaries(), + item -> auxiliaryTypeNameMap.get(item.getTypeId()) + ":" + item.getName()))); + copyAmounts(assist, row); + } + return row; + } + + private static void copyAmounts(FmsInitialBalanceRespVO source, FmsInitialBalanceExcelVO target) { + target.setOpeningAmount(zeroIfNull(source.getOpeningAmount())) + .setOpeningQuantity(zeroIfNull(source.getOpeningQuantity())) + .setYearDebitAmount(zeroIfNull(source.getYearDebitAmount())) + .setYearDebitQuantity(zeroIfNull(source.getYearDebitQuantity())) + .setYearCreditAmount(zeroIfNull(source.getYearCreditAmount())) + .setYearCreditQuantity(zeroIfNull(source.getYearCreditQuantity())) + .setYearOpeningAmount(zeroIfNull(source.getYearOpeningAmount())) + .setYearOpeningQuantity(zeroIfNull(source.getYearOpeningQuantity())) + .setProfitLossAmount(zeroIfNull(source.getProfitLossAmount())) + .setProfitLossQuantity(zeroIfNull(source.getProfitLossQuantity())); + } + + private static void copyAmounts(FmsInitialBalanceRespVO.AssistBalance source, + FmsInitialBalanceExcelVO target) { + target.setOpeningAmount(zeroIfNull(source.getOpeningAmount())) + .setOpeningQuantity(zeroIfNull(source.getOpeningQuantity())) + .setYearDebitAmount(zeroIfNull(source.getYearDebitAmount())) + .setYearDebitQuantity(zeroIfNull(source.getYearDebitQuantity())) + .setYearCreditAmount(zeroIfNull(source.getYearCreditAmount())) + .setYearCreditQuantity(zeroIfNull(source.getYearCreditQuantity())) + .setYearOpeningAmount(zeroIfNull(source.getYearOpeningAmount())) + .setYearOpeningQuantity(zeroIfNull(source.getYearOpeningQuantity())) + .setProfitLossAmount(zeroIfNull(source.getProfitLossAmount())) + .setProfitLossQuantity(zeroIfNull(source.getProfitLossQuantity())); + } + + /** + * 生成导入模板 + * + * 模板锁定系统生成列,年初余额列写入公式,并通过 protectSheet 防止误改 + */ + public static byte[] writeTemplate(List rows, + boolean january) throws IOException { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + // 1. 写入表头,一月启用时只保留期初余额 + Sheet sheet = workbook.createSheet(TEMPLATE_SHEET_NAME); + writeHeaders(workbook, sheet, 0, january); + // 2. 写入科目数据,系统生成列锁定,可填写列解锁 + CellStyle lockedStyle = createDataStyle(workbook, true, IndexedColors.GREY_25_PERCENT); + CellStyle editableStyle = createDataStyle(workbook, false, IndexedColors.WHITE); + CellStyle numberLockedStyle = createNumberStyle(workbook, lockedStyle, "#,##0.0000"); + CellStyle amountLockedStyle = createNumberStyle(workbook, lockedStyle, "#,##0.00"); + CellStyle numberEditableStyle = createNumberStyle(workbook, editableStyle, "#,##0.0000"); + CellStyle amountEditableStyle = createNumberStyle(workbook, editableStyle, "#,##0.00"); + for (int index = 0; index < rows.size(); index++) { + writeDataRow(sheet.createRow(index + 2), rows.get(index), january, true, + lockedStyle, editableStyle, numberLockedStyle, amountLockedStyle, + numberEditableStyle, amountEditableStyle); + } + // 3. 保护工作表并补充填写说明 + sheet.protectSheet("yudao-fms"); + sheet.createFreezePane(4, 2); + writeInstructions(workbook); + workbook.write(output); + return output.toByteArray(); + } + } + + /** + * 读取导入文件 + * + * 按模板表头校验文件合法性,年初余额等公式单元格取公式计算值;不限制导入行数 + */ + public static List read(MultipartFile file) throws IOException { + try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) { + // 1. 校验模板 sheet 和表头 + Sheet sheet = workbook.getSheet(TEMPLATE_SHEET_NAME); + if (sheet == null || !hasValidHeaders(sheet)) { + throw new IllegalArgumentException("初始余额模板表头不正确"); + } + // 2. 逐行解析,跳过空行 + FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); + boolean january = sheet.getRow(1).getLastCellNum() <= 6; + List rows = new ArrayList<>(); + for (int rowIndex = 2; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (isEmptyRow(row, evaluator, Arrays.asList(0, 1, 3, 4, 5))) { + continue; + } + rows.add(parseRow(row, january, evaluator)); + } + return rows; + } + } + + /** + * 写入双行表头,数量和金额成组;一月启用时只保留期初余额组 + */ + private static void writeHeaders(Workbook workbook, Sheet sheet, int rowIndex, boolean january) { + CellStyle style = createHeaderStyle(workbook, IndexedColors.GREY_25_PERCENT); + int lastColumn = january ? 5 : 13; + Row firstRow = sheet.createRow(rowIndex); + Row secondRow = sheet.createRow(rowIndex + 1); + mergeHeader(sheet, firstRow, secondRow, 0, 0, "科目编码", style); + mergeHeader(sheet, firstRow, secondRow, 1, 1, "科目名称", style); + mergeHeader(sheet, firstRow, secondRow, 2, 2, "方向", style); + mergeHeader(sheet, firstRow, secondRow, 3, 3, "辅助核算项目", style); + writeGroupHeader(sheet, firstRow, secondRow, 4, "期初余额", style); + if (!january) { + writeGroupHeader(sheet, firstRow, secondRow, 6, "本年累计借方", style); + writeGroupHeader(sheet, firstRow, secondRow, 8, "本年累计贷方", style); + writeGroupHeader(sheet, firstRow, secondRow, 10, "年初余额", style); + writeGroupHeader(sheet, firstRow, secondRow, 12, "实际损益发生额", style); + } + for (int column = 0; column <= lastColumn; column++) { + sheet.setColumnWidth(column, column == 1 || column == 3 ? 7_000 : 4_000); + } + firstRow.setHeightInPoints(28); + secondRow.setHeightInPoints(24); + } + + /** + * 写入“数量/金额”成组表头,第一行合并组名,第二行拆出数量和金额 + */ + private static void writeGroupHeader(Sheet sheet, Row firstRow, Row secondRow, + int beginColumn, String title, CellStyle style) { + sheet.addMergedRegion(new CellRangeAddress(firstRow.getRowNum(), firstRow.getRowNum(), beginColumn, beginColumn + 1)); + Cell groupCell = firstRow.createCell(beginColumn); + groupCell.setCellValue(title); + groupCell.setCellStyle(style); + firstRow.createCell(beginColumn + 1).setCellStyle(style); + Cell quantityCell = secondRow.createCell(beginColumn); + quantityCell.setCellValue("数量"); + quantityCell.setCellStyle(style); + Cell amountCell = secondRow.createCell(beginColumn + 1); + amountCell.setCellValue("金额"); + amountCell.setCellStyle(style); + } + + /** + * 写入一行数据 + * + * 模板模式下按科目配置解锁可填写单元格(数量列仅数量核算科目可填,损益列仅损益类科目可填), + * 年初余额列写入“期初 ± 累计借贷”公式 + */ + private static void writeDataRow(Row excelRow, FmsInitialBalanceExcelVO data, + boolean january, boolean template, CellStyle lockedStyle, CellStyle editableStyle, + CellStyle numberLockedStyle, CellStyle amountLockedStyle, + CellStyle numberEditableStyle, CellStyle amountEditableStyle) { + writeText(excelRow, 0, data.getSubjectCode(), lockedStyle); + writeText(excelRow, 1, data.getSubjectName(), lockedStyle); + writeText(excelRow, 2, data.getDirectionName(), lockedStyle); + writeText(excelRow, 3, data.getAuxiliaryItems(), + template && Boolean.TRUE.equals(data.getAuxiliaryAccounting()) ? editableStyle : lockedStyle); + writeNumber(excelRow, 4, data.getOpeningQuantity(), + template && Boolean.TRUE.equals(data.getQuantityAccounting()) + ? numberEditableStyle : numberLockedStyle); + writeNumber(excelRow, 5, data.getOpeningAmount(), template ? amountEditableStyle : amountLockedStyle); + if (january) { + return; + } + writeNumber(excelRow, 6, data.getYearDebitQuantity(), + template && Boolean.TRUE.equals(data.getQuantityAccounting()) + ? numberEditableStyle : numberLockedStyle); + writeNumber(excelRow, 7, data.getYearDebitAmount(), template ? amountEditableStyle : amountLockedStyle); + writeNumber(excelRow, 8, data.getYearCreditQuantity(), + template && Boolean.TRUE.equals(data.getQuantityAccounting()) + ? numberEditableStyle : numberLockedStyle); + writeNumber(excelRow, 9, data.getYearCreditAmount(), template ? amountEditableStyle : amountLockedStyle); + if (template) { + String quantityFormula = "借".equals(data.getDirectionName()) + ? "E" + (excelRow.getRowNum() + 1) + "-G" + (excelRow.getRowNum() + 1) + + "+I" + (excelRow.getRowNum() + 1) + : "E" + (excelRow.getRowNum() + 1) + "+G" + (excelRow.getRowNum() + 1) + + "-I" + (excelRow.getRowNum() + 1); + String amountFormula = "借".equals(data.getDirectionName()) + ? "F" + (excelRow.getRowNum() + 1) + "-H" + (excelRow.getRowNum() + 1) + + "+J" + (excelRow.getRowNum() + 1) + : "F" + (excelRow.getRowNum() + 1) + "+H" + (excelRow.getRowNum() + 1) + + "-J" + (excelRow.getRowNum() + 1); + writeFormula(excelRow, 10, quantityFormula, numberLockedStyle); + writeFormula(excelRow, 11, amountFormula, amountLockedStyle); + } else { + writeNumber(excelRow, 10, data.getYearOpeningQuantity(), numberLockedStyle); + writeNumber(excelRow, 11, data.getYearOpeningAmount(), amountLockedStyle); + } + boolean profitLoss = FmsSubjectTypeEnum.PROFIT_LOSS.getType().equals(data.getSubjectType()); + writeNumber(excelRow, 12, profitLoss ? data.getProfitLossQuantity() : null, + template && profitLoss && Boolean.TRUE.equals(data.getQuantityAccounting()) + ? numberEditableStyle : numberLockedStyle); + writeNumber(excelRow, 13, profitLoss ? data.getProfitLossAmount() : null, + template && profitLoss ? amountEditableStyle : amountLockedStyle); + } + + /** + * 解析一行数据,公式单元格通过 evaluator 取计算值 + */ + private static FmsInitialBalanceExcelVO parseRow(Row row, boolean january, FormulaEvaluator evaluator) { + FmsInitialBalanceExcelVO result = new FmsInitialBalanceExcelVO() + .setRowNumber(row.getRowNum() + 1) + .setSubjectCode(getCellText(row, 0, evaluator)) + .setSubjectName(getCellText(row, 1, evaluator)) + .setDirectionName(getCellText(row, 2, evaluator)) + .setAuxiliaryItems(getCellText(row, 3, evaluator)) + .setOpeningQuantity(getDecimal(row, 4, evaluator)) + .setOpeningAmount(getDecimal(row, 5, evaluator)); + if (!january) { + result.setYearDebitQuantity(getDecimal(row, 6, evaluator)) + .setYearDebitAmount(getDecimal(row, 7, evaluator)) + .setYearCreditQuantity(getDecimal(row, 8, evaluator)) + .setYearCreditAmount(getDecimal(row, 9, evaluator)) + .setYearOpeningQuantity(getDecimal(row, 10, evaluator)) + .setYearOpeningAmount(getDecimal(row, 11, evaluator)) + .setProfitLossQuantity(getDecimal(row, 12, evaluator)) + .setProfitLossAmount(getDecimal(row, 13, evaluator)); + } + return result; + } + + private static boolean hasValidHeaders(Sheet sheet) { + Row firstRow = sheet.getRow(0); + Row secondRow = sheet.getRow(1); + return firstRow != null && secondRow != null + && "科目编码".equals(getCellText(firstRow, 0, null)) + && "科目名称".equals(getCellText(firstRow, 1, null)) + && "方向".equals(getCellText(firstRow, 2, null)) + && "辅助核算项目".equals(getCellText(firstRow, 3, null)) + && "期初余额".equals(getCellText(firstRow, 4, null)) + && "数量".equals(getCellText(secondRow, 4, null)) + && "金额".equals(getCellText(secondRow, 5, null)); + } + + private static void writeInstructions(Workbook workbook) { + Sheet sheet = workbook.createSheet("填写说明"); + String[] instructions = { + "1. 灰色单元格由系统生成,请勿修改", + "2. 仅末级科目可以导入初始余额", + "3. 辅助核算项目使用“类别:名称/类别:名称”格式,例如“客户:北京客户/部门:销售部”", + "4. 数量核算科目可填写数量,金额和数量不能小于 0", + "5. 年初余额由期初余额和本年累计发生额自动计算" + }; + for (int index = 0; index < instructions.length; index++) { + sheet.createRow(index).createCell(0).setCellValue(instructions[index]); + } + sheet.setColumnWidth(0, 20_000); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsSubjectController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsSubjectController.java new file mode 100644 index 0000000..516b686 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsSubjectController.java @@ -0,0 +1,195 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectDeleteReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectStatusReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectUsageRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 会计科目") +@RestController +@RequestMapping("/fms/config/subject") +@Validated +public class FmsSubjectController { + + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + + @PostMapping("/create") + @Operation(summary = "创建科目") + @PreAuthorize("@ss.hasPermission('fms:config:subject:create')") + public CommonResult createSubject(@Valid @RequestBody FmsSubjectSaveReqVO createReqVO) { + return success(subjectService.createSubject(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新科目") + @PreAuthorize("@ss.hasPermission('fms:config:subject:update')") + public CommonResult updateSubject(@Valid @RequestBody FmsSubjectSaveReqVO updateReqVO) { + subjectService.updateSubject(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "删除科目") + @PreAuthorize("@ss.hasPermission('fms:config:subject:delete')") + public CommonResult deleteSubjectList(@Valid @RequestBody FmsSubjectDeleteReqVO deleteReqVO) { + subjectService.deleteSubjectList(deleteReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新科目状态") + @PreAuthorize("@ss.hasPermission('fms:config:subject:update')") + public CommonResult updateSubjectStatus(@Valid @RequestBody FmsSubjectStatusReqVO statusReqVO) { + subjectService.updateSubjectStatus(statusReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得科目") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "id", description = "科目编号", required = true, example = "1024") + }) + @PreAuthorize("@ss.hasPermission('fms:config:subject:query')") + public CommonResult getSubject(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + FmsSubjectDO subject = subjectService.getSubject(accountSetId, id, getLoginUserId()); + return success(buildSubjectRespVO(subject)); + } + + @GetMapping("/get-usage") + @Operation(summary = "获得科目使用情况") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "id", description = "科目编号", required = true, example = "1024") + }) + @PreAuthorize("@ss.hasPermission('fms:config:subject:query')") + public CommonResult getSubjectUsage( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + return success(subjectService.getSubjectUsage(accountSetId, id, getLoginUserId())); + } + + @GetMapping("/list") + @Operation(summary = "获得科目列表") + @PreAuthorize("@ss.hasPermission('fms:config:subject:query')") + public CommonResult> getSubjectList(@Valid FmsSubjectListReqVO listReqVO) { + List subjects = subjectService.getSubjectList(listReqVO, getLoginUserId()); + return success(buildSubjectRespVOList(subjects)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得科目精简列表") + public CommonResult> getSubjectSimpleList(@Valid FmsSubjectListReqVO listReqVO) { + List subjects = subjectService.getSubjectList(listReqVO, getLoginUserId()); + return success(convertList(subjects, subject -> new FmsSubjectRespVO() + .setId(subject.getId()).setParentId(subject.getParentId()) + .setCode(subject.getCode()).setName(subject.getName()).setType(subject.getType()) + .setBalanceDirection(subject.getBalanceDirection()).setStatus(subject.getStatus()) + .setLevel(subject.getLevel()).setAuxiliaryTypeIds(subject.getAuxiliaryTypeIds()) + .setQuantityAccounting(subject.getQuantityAccounting()).setQuantityUnit(subject.getQuantityUnit()))); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出科目") + @PreAuthorize("@ss.hasPermission('fms:config:subject:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportSubject(@Valid FmsSubjectListReqVO listReqVO, + HttpServletResponse response) throws IOException { + List subjects = subjectService.getSubjectList(listReqVO, getLoginUserId()); + ExcelUtils.write(response, "科目设置.xlsx", "科目设置", FmsSubjectRespVO.class, + buildSubjectRespVOList(subjects)); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得科目导入模板") + @PreAuthorize("@ss.hasPermission('fms:config:subject:import')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getSubjectImportTemplate(HttpServletResponse response) throws IOException { + List list = Arrays.asList( + FmsSubjectImportExcelVO.builder().code("1001").name("库存现金") + .parentSubjectCode("0").balanceDirection("借").categoryName("流动资产").build(), + FmsSubjectImportExcelVO.builder().code("100101").name("人民币现金") + .parentSubjectCode("1001").balanceDirection("借").categoryName("流动资产") + .auxiliaryNames("部门/职员").build()); + ExcelUtils.write(response, "科目导入模板.xlsx", "科目列表", FmsSubjectImportExcelVO.class, list); + } + + @PostMapping("/import") + @Operation(summary = "导入科目") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "file", description = "Excel 文件", required = true) + }) + @PreAuthorize("@ss.hasPermission('fms:config:subject:import')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult importSubject( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("file") MultipartFile file) throws IOException { + List importSubjects = ExcelUtils.read(file, FmsSubjectImportExcelVO.class); + return success(subjectService.importSubjectList(accountSetId, importSubjects, getLoginUserId())); + } + + // ==================== 拼接 VO ==================== + + private List buildSubjectRespVOList(List subjects) { + Map auxiliaryTypeMap = auxiliaryTypeService.getAuxiliaryTypeMap( + convertSetByFlatMap(subjects, FmsSubjectDO::getAuxiliaryTypeIds, List::stream)); + return convertList(subjects, subject -> { + FmsSubjectRespVO subjectVO = BeanUtils.toBean(subject, FmsSubjectRespVO.class) + .setCategoryDictValue(subject.getType() + "-" + subject.getCategory()); + subjectVO.setAuxiliaryTypeNames(convertList(subject.getAuxiliaryTypeIds(), + auxiliaryTypeId -> auxiliaryTypeMap.get(auxiliaryTypeId).getName())); + if (Boolean.FALSE.equals(subject.getQuantityAccounting())) { + subjectVO.setQuantityUnit(""); + } + return subjectVO; + }); + } + + private FmsSubjectRespVO buildSubjectRespVO(FmsSubjectDO subject) { + return subject == null ? null + : CollUtil.getFirst(buildSubjectRespVOList(Collections.singletonList(subject))); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherTemplateCategoryController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherTemplateCategoryController.java new file mode 100644 index 0000000..6201f0d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherTemplateCategoryController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory.FmsVoucherTemplateCategoryRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory.FmsVoucherTemplateCategorySaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 凭证模板分类") +@RestController +@RequestMapping("/fms/config/voucher-template-category") +@Validated +public class FmsVoucherTemplateCategoryController { + + @Resource + private FmsVoucherTemplateService voucherTemplateService; + @Resource + private FmsAccountSetService accountSetService; + + @PostMapping("/create") + @Operation(summary = "创建凭证模板分类") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:create')") + public CommonResult createTemplateCategory( + @Valid @RequestBody FmsVoucherTemplateCategorySaveReqVO createReqVO) { + return success(voucherTemplateService.createTemplateCategory(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改凭证模板分类") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:update')") + public CommonResult updateTemplateCategory( + @Valid @RequestBody FmsVoucherTemplateCategorySaveReqVO updateReqVO) { + voucherTemplateService.updateTemplateCategory(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除凭证模板分类") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:delete')") + public CommonResult deleteTemplateCategory(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + voucherTemplateService.deleteTemplateCategory(accountSetId, id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得凭证模板分类列表") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:query')") + public CommonResult> getTemplateCategoryList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId()); + List categories = voucherTemplateService.getTemplateCategoryList(accountSetId); + return success(BeanUtils.toBean(categories, FmsVoucherTemplateCategoryRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得凭证模板分类精简列表", description = "主要用于前端的下拉选项") + public CommonResult> getTemplateCategorySimpleList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId()); + List categories = voucherTemplateService.getTemplateCategoryList(accountSetId); + return success(convertList(categories, category -> new FmsVoucherTemplateCategoryRespVO() + .setId(category.getId()).setName(category.getName()))); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherTemplateController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherTemplateController.java new file mode 100644 index 0000000..e5a8ce2 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherTemplateController.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 凭证模板") +@RestController +@RequestMapping("/fms/config/voucher-template") +@Validated +public class FmsVoucherTemplateController { + + @Resource + private FmsVoucherTemplateService voucherTemplateService; + @Resource + private FmsAccountSetService accountSetService; + + @PostMapping("/create") + @Operation(summary = "创建凭证模板") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template:create')") + public CommonResult createVoucherTemplate( + @Valid @RequestBody FmsVoucherTemplateSaveReqVO createReqVO) { + return success(voucherTemplateService.createVoucherTemplate(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改凭证模板") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template:update')") + public CommonResult updateVoucherTemplate( + @Valid @RequestBody FmsVoucherTemplateSaveReqVO updateReqVO) { + voucherTemplateService.updateVoucherTemplate(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除凭证模板") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template:delete')") + public CommonResult deleteVoucherTemplate(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + voucherTemplateService.deleteVoucherTemplate(accountSetId, id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得凭证模板列表") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-template:query')") + public CommonResult> getVoucherTemplateList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId()); + return success(buildVoucherTemplateRespVOList(accountSetId)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得凭证模板精简列表", description = "主要用于录凭证时套用凭证模板") + public CommonResult> getVoucherTemplateSimpleList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId()); + return success(buildVoucherTemplateRespVOList(accountSetId)); + } + + // ==================== 拼接 VO ==================== + + private List buildVoucherTemplateRespVOList(Long accountSetId) { + Map categoryMap = + voucherTemplateService.getTemplateCategoryMap(accountSetId); + List templates = voucherTemplateService.getVoucherTemplateList(accountSetId); + return BeanUtils.toBean(templates, FmsVoucherTemplateRespVO.class, template -> { + findAndThen(categoryMap, template.getCategoryId(), + category -> template.setCategoryName(category.getName())); + }); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherWordController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherWordController.java new file mode 100644 index 0000000..00e8ce6 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/FmsVoucherWordController.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword.FmsVoucherWordRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword.FmsVoucherWordSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * FMS 凭证字 Controller + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - FMS 凭证字") +@RestController +@RequestMapping("/fms/config/voucher-word") +@Validated +public class FmsVoucherWordController { + + @Resource + private FmsVoucherWordService voucherWordService; + + @PostMapping("/create") + @Operation(summary = "创建凭证字") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-word:create')") + public CommonResult createVoucherWord(@Valid @RequestBody FmsVoucherWordSaveReqVO createReqVO) { + return success(voucherWordService.createVoucherWord(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改凭证字") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-word:update')") + public CommonResult updateVoucherWord(@Valid @RequestBody FmsVoucherWordSaveReqVO updateReqVO) { + voucherWordService.updateVoucherWord(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除凭证字") + @Parameter(name = "id", description = "凭证字编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-word:delete')") + public CommonResult deleteVoucherWord( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + voucherWordService.deleteVoucherWord(accountSetId, id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得凭证字管理列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:config:voucher-word:query')") + public CommonResult> getVoucherWordList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + List list = voucherWordService.getVoucherWordList(accountSetId, getLoginUserId()); + return success(BeanUtils.toBean(list, FmsVoucherWordRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得凭证字精简列表") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + public CommonResult> getVoucherWordSimpleList( + @RequestParam("accountSetId") @NotNull Long accountSetId) { + return success(convertList(voucherWordService.getVoucherWordList( + accountSetId, getLoginUserId()), voucherWord -> new FmsVoucherWordRespVO() + .setId(voucherWord.getId()).setName(voucherWord.getName()) + .setDefaultStatus(voucherWord.getDefaultStatus()))); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetInitializeReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetInitializeReqVO.java new file mode 100644 index 0000000..ac0996c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetInitializeReqVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountingStandardEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsCurrencyPresetEnum; +import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - FMS 账套初始化 Request VO") +@Data +public class FmsAccountSetInitializeReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "本位币编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "RMB") + @NotBlank(message = "本位币不能为空") + @InEnum(value = FmsCurrencyPresetEnum.class, message = "本位币必须是 {value}") + private String currencyCode; + + @Schema(description = "启用期间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "启用期间不能为空") + private LocalDateTime startTime; + + @Schema(description = "会计制度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "会计制度不能为空") + @InEnum(value = FmsAccountingStandardEnum.class, message = "会计制度必须是 {value}") + private Integer standard; + + @Schema(description = "科目层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + @NotNull(message = "科目层级不能为空") + @Min(value = 1, message = "科目层级不能小于 1") + @Max(value = 8, message = "科目层级不能大于 8") + private Integer level; + + @Schema(description = "科目编码规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "4-2-2-2") + @NotBlank(message = "科目编码规则不能为空") + @Size(max = 64, message = "科目编码规则不能超过 64 个字符") + private String subjectCodeRule; + + @Schema(description = "账簿余额方向模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账簿余额方向模式不能为空") + @InEnum(FmsLedgerBalanceModeEnum.class) + private Integer ledgerBalanceMode; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetRespVO.java new file mode 100644 index 0000000..7ae198a --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetRespVO.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - FMS 账套 Response VO") +@Data +public class FmsAccountSetRespVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公司编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WK001") + private String companyCode; + + @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试科技有限公司") + private String companyName; + + @Schema(description = "公司简介", example = "专注企业数字化服务") + private String companyProfile; + + @Schema(description = "所在行业", example = "软件和信息技术服务业") + private String industry; + + @Schema(description = "所在地", example = "上海市浦东新区") + private String location; + + @Schema(description = "法人代表", example = "张三") + private String legalRepresentative; + + @Schema(description = "法人身份证号", example = "310101199001011234") + private String legalRepresentativeIdNumber; + + @Schema(description = "营业执照号", example = "91310000MA1K123456") + private String businessLicenseNumber; + + @Schema(description = "组织机构代码", example = "MA1K12345") + private String organizationCode; + + @Schema(description = "备注", example = "一般纳税人") + private String remark; + + @Schema(description = "联系人", example = "李四") + private String contactName; + + @Schema(description = "办公电话", example = "021-12345678") + private String officeTelephone; + + @Schema(description = "手机号码", example = "15601691399") + private String mobile; + + @Schema(description = "传真号码", example = "021-87654321") + private String faxNumber; + + @Schema(description = "QQ 号码", example = "12345678") + private String qqNumber; + + @Schema(description = "邮箱", example = "finance@example.com") + private String email; + + @Schema(description = "其他联系方式", example = "企业微信:wk-finance") + private String otherContact; + + @Schema(description = "详细地址", example = "张江高科技园区 1 号楼") + private String address; + + @Schema(description = "币种编号", example = "1024") + private Long currencyId; + + @Schema(description = "启用期间") + private LocalDateTime startTime; + + @Schema(description = "会计制度", example = "1") + private Integer standard; + + @Schema(description = "是否已初始化", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean initialized; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + // ==================== 来自 FmsAccountUserDO ==================== + + @Schema(description = "是否默认账套", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean defaultStatus; + + @Schema(description = "是否账套创建人", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean founder; + + @Schema(description = "当前用户的成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetSaveReqVO.java new file mode 100644 index 0000000..10b1601 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountset/FmsAccountSetSaveReqVO.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 账套新增/修改 Request VO") +@Data +public class FmsAccountSetSaveReqVO { + + @Schema(description = "账套编号", example = "1024") + private Long id; + + @Schema(description = "公司编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WK001") + @NotBlank(message = "公司编码不能为空") + @Size(max = 64, message = "公司编码不能超过 64 个字符") + @DiffLogField(name = "公司编码") + private String companyCode; + + @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试科技有限公司") + @NotBlank(message = "公司名称不能为空") + @Size(max = 255, message = "公司名称不能超过 255 个字符") + @DiffLogField(name = "公司名称") + private String companyName; + + @Schema(description = "公司简介", example = "专注企业数字化服务") + @Size(max = 500, message = "公司简介不能超过 500 个字符") + @DiffLogField(name = "公司简介") + private String companyProfile; + + @Schema(description = "所在行业", example = "软件和信息技术服务业") + @Size(max = 255, message = "所在行业不能超过 255 个字符") + @DiffLogField(name = "所在行业") + private String industry; + + @Schema(description = "所在地", example = "上海市浦东新区") + @Size(max = 255, message = "所在地不能超过 255 个字符") + @DiffLogField(name = "所在地") + private String location; + + @Schema(description = "法人代表", example = "张三") + @Size(max = 255, message = "法人代表不能超过 255 个字符") + @DiffLogField(name = "法人代表") + private String legalRepresentative; + + @Schema(description = "法人身份证号", example = "310101199001011234") + @Size(max = 255, message = "法人身份证号不能超过 255 个字符") + @DiffLogField(name = "法人身份证号") + private String legalRepresentativeIdNumber; + + @Schema(description = "营业执照号", example = "91310000MA1K123456") + @Size(max = 255, message = "营业执照号不能超过 255 个字符") + @DiffLogField(name = "营业执照号") + private String businessLicenseNumber; + + @Schema(description = "组织机构代码", example = "MA1K12345") + @Size(max = 255, message = "组织机构代码不能超过 255 个字符") + @DiffLogField(name = "组织机构代码") + private String organizationCode; + + @Schema(description = "备注", example = "一般纳税人") + @Size(max = 500, message = "备注不能超过 500 个字符") + @DiffLogField(name = "备注") + private String remark; + + @Schema(description = "联系人", example = "李四") + @Size(max = 255, message = "联系人不能超过 255 个字符") + @DiffLogField(name = "联系人") + private String contactName; + + @Schema(description = "办公电话", example = "021-12345678") + @Size(max = 32, message = "办公电话不能超过 32 个字符") + @DiffLogField(name = "办公电话") + private String officeTelephone; + + @Schema(description = "手机号码", example = "15601691399") + @Size(max = 32, message = "手机号码不能超过 32 个字符") + @DiffLogField(name = "手机号码") + private String mobile; + + @Schema(description = "传真号码", example = "021-87654321") + @Size(max = 32, message = "传真号码不能超过 32 个字符") + @DiffLogField(name = "传真号码") + private String faxNumber; + + @Schema(description = "QQ 号码", example = "12345678") + @Size(max = 255, message = "QQ 号码不能超过 255 个字符") + @DiffLogField(name = "QQ 号码") + private String qqNumber; + + @Schema(description = "邮箱", example = "finance@example.com") + @Email(message = "邮箱格式不正确") + @Size(max = 255, message = "邮箱不能超过 255 个字符") + @DiffLogField(name = "邮箱") + private String email; + + @Schema(description = "其他联系方式", example = "企业微信:wk-finance") + @Size(max = 255, message = "其他联系方式不能超过 255 个字符") + @DiffLogField(name = "其他联系方式") + private String otherContact; + + @Schema(description = "详细地址", example = "张江高科技园区 1 号楼") + @Size(max = 255, message = "详细地址不能超过 255 个字符") + @DiffLogField(name = "详细地址") + private String address; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountuser/FmsAccountUserRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountuser/FmsAccountUserRespVO.java new file mode 100644 index 0000000..d841f70 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountuser/FmsAccountUserRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 账套用户 Response VO") +@Data +public class FmsAccountUserRespVO { + + @Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "用户昵称", example = "芋道") + private String nickname; + + @Schema(description = "部门名称", example = "财务部") + private String deptName; + + @Schema(description = "手机号码", example = "15601691399") + private String mobile; + + @Schema(description = "用户邮箱", example = "finance@example.com") + private String email; + + @Schema(description = "用户状态", example = "0") + private Integer status; + + @Schema(description = "是否默认账套", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean defaultStatus; + + @Schema(description = "是否账套创建人", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean founder; + + @Schema(description = "成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountuser/FmsAccountUserUpdateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountuser/FmsAccountUserUpdateReqVO.java new file mode 100644 index 0000000..16565f1 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/accountuser/FmsAccountUserUpdateReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountUserLevelEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 账套用户更新 Request VO") +@Data +public class FmsAccountUserUpdateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "账套成员数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "账套成员数组不能为空") + @Size(max = 100, message = "单个账套最多授权 100 名成员") + @Valid + private List members; + + @Schema(description = "管理后台 - FMS 账套成员") + @Data + public static class Member { + + @Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "成员用户编号不能为空") + private Long userId; + + @Schema(description = "成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "成员权限级别不能为空") + @InEnum(FmsAccountUserLevelEnum.class) + private Integer level; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemImportExcelVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemImportExcelVO.java new file mode 100644 index 0000000..6a15e3c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemImportExcelVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem; + +import cn.idev.excel.annotation.ExcelProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * FMS 辅助核算项目 Excel 导入 VO + * + * @author 芋道源码 + */ +@Data +public class FmsAuxiliaryItemImportExcelVO { + + @ExcelProperty("编码") + @NotBlank(message = "编码不能为空") + @Size(max = 64, message = "编码长度不能超过 64 个字符") + private String code; + + @ExcelProperty("名称") + @NotBlank(message = "名称不能为空") + @Size(max = 255, message = "名称长度不能超过 255 个字符") + private String name; + + @ExcelProperty("备注") + @Size(max = 500, message = "备注长度不能超过 500 个字符") + private String remark; + + @ExcelProperty("规格") + @Size(max = 255, message = "规格长度不能超过 255 个字符") + private String specification; + + @ExcelProperty("单位") + @Size(max = 255, message = "单位长度不能超过 255 个字符") + private String unit; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemImportRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemImportRespVO.java new file mode 100644 index 0000000..8cb7a35 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemImportRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * FMS 辅助核算项目导入结果 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 辅助核算项目导入结果 Response VO") +@Data +public class FmsAuxiliaryItemImportRespVO { + + @Schema(description = "总数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer totalCount; + + @Schema(description = "成功编码数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List successItemCodes; + + @Schema(description = "失败原因 Map", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureReasons; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemPageReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemPageReqVO.java new file mode 100644 index 0000000..2707058 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * FMS 辅助核算项目分页 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 辅助核算项目分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class FmsAuxiliaryItemPageReqVO extends PageParam { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "辅助核算类别编号不能为空") + private Long auxiliaryTypeId; + + @Schema(description = "关键词,匹配编码或名称", example = "测试") + private String search; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemRespVO.java new file mode 100644 index 0000000..f41a550 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemRespVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * FMS 辅助核算项目 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 辅助核算项目 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsAuxiliaryItemRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountSetId; + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long auxiliaryTypeId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "KH001") + @ExcelProperty("编码") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海测试数字科技有限公司") + @ExcelProperty("名称") + private String name; + + @Schema(description = "备注", example = "重点客户") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "规格", example = "标准版") + @ExcelProperty("规格") + private String specification; + + @Schema(description = "单位", example = "台") + @ExcelProperty("单位") + private String unit; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemSaveReqVO.java new file mode 100644 index 0000000..0d16e1b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemSaveReqVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * FMS 辅助核算项目创建或修改 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 辅助核算项目创建或修改 Request VO") +@Data +public class FmsAuxiliaryItemSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "辅助核算类别编号不能为空") + private Long auxiliaryTypeId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "KH001") + @NotBlank(message = "编码不能为空") + @Size(max = 64, message = "编码长度不能超过 64 个字符") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海测试数字科技有限公司") + @NotBlank(message = "名称不能为空") + @Size(max = 255, message = "名称长度不能超过 255 个字符") + private String name; + + @Schema(description = "备注", example = "重点客户") + @Size(max = 500, message = "备注长度不能超过 500 个字符") + private String remark; + + @Schema(description = "规格", example = "标准版") + @Size(max = 255, message = "规格长度不能超过 255 个字符") + private String specification; + + @Schema(description = "单位", example = "台") + @Size(max = 255, message = "单位长度不能超过 255 个字符") + private String unit; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemStatusReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemStatusReqVO.java new file mode 100644 index 0000000..ed29d9e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemStatusReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * FMS 辅助核算项目状态更新 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 辅助核算项目状态更新 Request VO") +@Data +public class FmsAuxiliaryItemStatusReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliarytype/FmsAuxiliaryTypeRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliarytype/FmsAuxiliaryTypeRespVO.java new file mode 100644 index 0000000..ba027b7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliarytype/FmsAuxiliaryTypeRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * FMS 辅助核算类别 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 辅助核算类别 Response VO") +@Data +public class FmsAuxiliaryTypeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户") + private String name; + + @Schema(description = "是否系统预置", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean systemPreset; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountSetId; + + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliarytype/FmsAuxiliaryTypeSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliarytype/FmsAuxiliaryTypeSaveReqVO.java new file mode 100644 index 0000000..db5508c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliarytype/FmsAuxiliaryTypeSaveReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * FMS 辅助核算类别创建或修改 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 辅助核算类别创建或修改 Request VO") +@Data +public class FmsAuxiliaryTypeSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "门店") + @NotBlank(message = "名称不能为空") + @Size(max = 255, message = "名称长度不能超过 255 个字符") + private String name; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/currency/FmsCurrencyRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/currency/FmsCurrencyRespVO.java new file mode 100644 index 0000000..9c7a6a8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/currency/FmsCurrencyRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * FMS 币别 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 币别 Response VO") +@Data +public class FmsCurrencyRespVO { + + @Schema(description = "币别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountSetId; + + @Schema(description = "币别编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "USD") + private String code; + + @Schema(description = "币别名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "美元") + private String name; + + @Schema(description = "汇率", requiredMode = Schema.RequiredMode.REQUIRED, example = "7.120000") + private BigDecimal exchangeRate; + + @Schema(description = "是否本位币", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean standard; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/currency/FmsCurrencySaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/currency/FmsCurrencySaveReqVO.java new file mode 100644 index 0000000..7839f87 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/currency/FmsCurrencySaveReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * FMS 币别保存 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 币别保存 Request VO") +@Data +public class FmsCurrencySaveReqVO { + + @Schema(description = "币别编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "币别编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "USD") + @NotBlank(message = "币别编码不能为空") + @Size(max = 64, message = "币别编码长度不能超过 64 个字符") + @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_]*$", message = "币别编码必须以字母开头,只能包含字母、数字和下划线") + private String code; + + @Schema(description = "币别名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "美元") + @NotBlank(message = "币别名称不能为空") + @Size(max = 255, message = "币别名称长度不能超过 255 个字符") + private String name; + + @Schema(description = "汇率", requiredMode = Schema.RequiredMode.REQUIRED, example = "7.120000") + @NotNull(message = "汇率不能为空") + @DecimalMin(value = "0", inclusive = false, message = "汇率必须大于 0") + @Digits(integer = 12, fraction = 6, message = "汇率最多 12 位整数和 6 位小数") + private BigDecimal exchangeRate; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/digest/FmsDigestRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/digest/FmsDigestRespVO.java new file mode 100644 index 0000000..029e16d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/digest/FmsDigestRespVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * FMS 常用摘要 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 常用摘要 Response VO") +@Data +public class FmsDigestRespVO { + + @Schema(description = "常用摘要编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountSetId; + + @Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "购买办公用品") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/digest/FmsDigestSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/digest/FmsDigestSaveReqVO.java new file mode 100644 index 0000000..2b296a0 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/digest/FmsDigestSaveReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * FMS 常用摘要保存 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 常用摘要保存 Request VO") +@Data +public class FmsDigestSaveReqVO { + + @Schema(description = "常用摘要编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "购买办公用品") + @NotBlank(message = "摘要内容不能为空") + @Size(max = 500, message = "摘要内容长度不能超过 500 个字符") + private String content; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeindicator/FmsFinanceIndicatorRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeindicator/FmsFinanceIndicatorRespVO.java new file mode 100644 index 0000000..410e1c3 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeindicator/FmsFinanceIndicatorRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +// TODO DONE @AI:字段 Swagger 注解和空行已补齐。 +@Schema(description = "管理后台 - FMS 财务指标 Response VO") +@Data +public class FmsFinanceIndicatorRespVO { + + @Schema(description = "指标编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", example = "1") + private Long accountSetId; + + @Schema(description = "指标名称", example = "现金") + private String name; + + @Schema(description = "指标编码", example = "cash") + private String code; + + @Schema(description = "取数报表类型", example = "1") + private Integer type; + + @Schema(description = "指标公式") + private String formula; + + @Schema(description = "展示顺序", example = "10") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeindicator/FmsFinanceIndicatorSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeindicator/FmsFinanceIndicatorSaveReqVO.java new file mode 100644 index 0000000..91eabdc --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeindicator/FmsFinanceIndicatorSaveReqVO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsFinanceIndicatorTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +// TODO DONE @AI:字段空行已对齐现有 SaveReqVO。 +@Schema(description = "管理后台 - FMS 财务指标保存 Request VO") +@Data +public class FmsFinanceIndicatorSaveReqVO { + + @Schema(description = "指标编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "现金") + @NotBlank(message = "指标名称不能为空") + @Size(max = 100, message = "指标名称长度不能超过 100 个字符") + private String name; + + @Schema(description = "指标编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "cash") + @NotBlank(message = "指标编码不能为空") + @Size(max = 64, message = "指标编码长度不能超过 64 个字符") + private String code; + + @Schema(description = "取数报表类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "取数报表类型不能为空") + @InEnum(FmsFinanceIndicatorTypeEnum.class) + private Integer type; + + @Schema(description = "指标公式,支持 L1+L2 或科目公式 JSON", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "指标公式不能为空") + @Size(max = 2000, message = "指标公式长度不能超过 2000 个字符") + private String formula; + + @Schema(description = "展示顺序", example = "10") + @NotNull(message = "展示顺序不能为空") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeparameter/FmsFinanceParameterRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeparameter/FmsFinanceParameterRespVO.java new file mode 100644 index 0000000..2e31efb --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeparameter/FmsFinanceParameterRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 财务参数 Response VO") +@Data +public class FmsFinanceParameterRespVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountSetId; + + @Schema(description = "科目层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + private Integer level; + + @Schema(description = "科目编码规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "4-2-2-2") + private String subjectCodeRule; + + @Schema(description = "账簿余额方向模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer ledgerBalanceMode; + + @Schema(description = "结账前是否要求凭证审核", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean voucherReviewRequired; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeparameter/FmsFinanceParameterUpdateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeparameter/FmsFinanceParameterUpdateReqVO.java new file mode 100644 index 0000000..6299af4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/financeparameter/FmsFinanceParameterUpdateReqVO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountingStandardEnum; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 财务参数更新 Request VO") +@Data +public class FmsFinanceParameterUpdateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "会计制度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "会计制度不能为空") + @InEnum(FmsAccountingStandardEnum.class) + @DiffLogField(name = "会计制度") + private Integer standard; + + @Schema(description = "科目层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + @NotNull(message = "科目层级不能为空") + @Min(value = 1, message = "科目层级不能小于 1") + @Max(value = 8, message = "科目层级不能大于 8") + @DiffLogField(name = "科目层级") + private Integer level; + + @Schema(description = "科目编码规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "4-2-2-2") + @NotBlank(message = "科目编码规则不能为空") + @Size(max = 64, message = "科目编码规则不能超过 64 个字符") + @DiffLogField(name = "科目编码规则") + private String subjectCodeRule; + + @Schema(description = "账簿余额方向模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账簿余额方向模式不能为空") + @InEnum(FmsLedgerBalanceModeEnum.class) + @DiffLogField(name = "账簿余额方向") + private Integer ledgerBalanceMode; + + @Schema(description = "结账前是否要求凭证审核", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "结账前凭证审核设置不能为空") + @DiffLogField(name = "凭证审核后才允许结账") + private Boolean voucherReviewRequired; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceExcelVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceExcelVO.java new file mode 100644 index 0000000..df1e229 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceExcelVO.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * FMS 初始余额 Excel VO + * + * @author 芋道源码 + */ +@Data +@ExcelIgnoreUnannotated +public class FmsInitialBalanceExcelVO { + + /** + * Excel 行号 + */ + private Integer rowNumber; + /** + * 科目编码 + */ + @ExcelProperty("科目编码") + private String subjectCode; + /** + * 科目名称 + */ + @ExcelProperty("科目名称") + private String subjectName; + /** + * 余额方向名称 + */ + @ExcelProperty("方向") + private String directionName; + /** + * 辅助核算项目,格式为“类别:名称/类别:名称” + */ + @ExcelProperty("辅助核算项目") + private String auxiliaryItems; + /** + * 科目类型 + */ + private Integer subjectType; + /** + * 是否启用数量核算 + */ + private Boolean quantityAccounting; + /** + * 是否启用辅助核算 + */ + private Boolean auxiliaryAccounting; + /** + * 期初数量 + */ + @ExcelProperty("期初数量") + private BigDecimal openingQuantity; + /** + * 期初金额 + */ + @ExcelProperty("期初金额") + private BigDecimal openingAmount; + /** + * 本年累计借方数量 + */ + @ExcelProperty("本年累计借方数量") + private BigDecimal yearDebitQuantity; + /** + * 本年累计借方金额 + */ + @ExcelProperty("本年累计借方金额") + private BigDecimal yearDebitAmount; + /** + * 本年累计贷方数量 + */ + @ExcelProperty("本年累计贷方数量") + private BigDecimal yearCreditQuantity; + /** + * 本年累计贷方金额 + */ + @ExcelProperty("本年累计贷方金额") + private BigDecimal yearCreditAmount; + /** + * 年初数量 + */ + @ExcelProperty("年初数量") + private BigDecimal yearOpeningQuantity; + /** + * 年初金额 + */ + @ExcelProperty("年初金额") + private BigDecimal yearOpeningAmount; + /** + * 实际损益发生数量 + */ + @ExcelProperty("实际损益发生数量") + private BigDecimal profitLossQuantity; + /** + * 实际损益发生金额 + */ + @ExcelProperty("实际损益发生金额") + private BigDecimal profitLossAmount; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceRespVO.java new file mode 100644 index 0000000..8a8cfc2 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceRespVO.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 初始余额 Response VO") +@Data +public class FmsInitialBalanceRespVO { + + @Schema(description = "初始余额编号", example = "1024") + private Long id; + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String subjectCode; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String subjectName; + + @Schema(description = "上级科目编号", example = "1024") + private Long parentId; + + @Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer type; + + @Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer balanceDirection; + + @Schema(description = "是否启用数量核算", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean quantityAccounting; + + @Schema(description = "数量单位") + private String quantityUnit; + + @Schema(description = "是否启用辅助核算", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean auxiliaryAccounting; + + @Schema(description = "期初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00") + private BigDecimal openingAmount; + + @Schema(description = "期初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000") + private BigDecimal openingQuantity; + + @Schema(description = "本年累计借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal yearDebitAmount; + + @Schema(description = "本年累计借方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000") + private BigDecimal yearDebitQuantity; + + @Schema(description = "本年累计贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "50.00") + private BigDecimal yearCreditAmount; + + @Schema(description = "本年累计贷方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000") + private BigDecimal yearCreditQuantity; + + @Schema(description = "年初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "950.00") + private BigDecimal yearOpeningAmount; + + @Schema(description = "年初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000") + private BigDecimal yearOpeningQuantity; + + @Schema(description = "实际损益发生额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00") + private BigDecimal profitLossAmount; + + @Schema(description = "实际损益发生数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000") + private BigDecimal profitLossQuantity; + + @Schema(description = "辅助核算配置数组") + private List auxiliaryConfigs; + + @Schema(description = "辅助核算余额数组") + private List assistBalances; + + @Schema(description = "管理后台 - FMS 初始余额辅助核算配置") + @Data + public static class AuxiliaryConfig { + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long auxiliaryTypeId; + + @Schema(description = "辅助核算类型", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer type; + + @Schema(description = "辅助核算类别名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + } + + @Schema(description = "管理后台 - FMS 初始余额辅助核算余额") + @Data + public static class AssistBalance { + + @Schema(description = "辅助核算组合编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long assistCombinationId; + + @Schema(description = "辅助核算项目数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List auxiliaries; + + @Schema(description = "期初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00") + private BigDecimal openingAmount; + + @Schema(description = "期初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000") + private BigDecimal openingQuantity; + + @Schema(description = "本年累计借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal yearDebitAmount; + + @Schema(description = "本年累计借方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000") + private BigDecimal yearDebitQuantity; + + @Schema(description = "本年累计贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "50.00") + private BigDecimal yearCreditAmount; + + @Schema(description = "本年累计贷方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000") + private BigDecimal yearCreditQuantity; + + @Schema(description = "年初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "950.00") + private BigDecimal yearOpeningAmount; + + @Schema(description = "年初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000") + private BigDecimal yearOpeningQuantity; + + @Schema(description = "实际损益发生额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00") + private BigDecimal profitLossAmount; + + @Schema(description = "实际损益发生数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000") + private BigDecimal profitLossQuantity; + + } + + @Schema(description = "管理后台 - FMS 初始余额辅助核算项目") + @Data + public static class AuxiliaryItem { + + @Schema(description = "辅助核算类型", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer type; + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long typeId; + + @Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long itemId; + + @Schema(description = "辅助核算项目名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceSaveReqVO.java new file mode 100644 index 0000000..6f8b2a7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceSaveReqVO.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 初始余额保存 Request VO") +@Data +public class FmsInitialBalanceSaveReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "科目余额数组", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "科目余额不能为空") + private List balances; + + @Data + public static class Balance extends Amounts { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "科目编号不能为空") + private Long subjectId; + + @Schema(description = "辅助核算余额数组") + @Valid + private List assistBalances; + + } + + @Data + public static class AssistBalance extends Amounts { + + @Schema(description = "辅助核算项目编号数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "辅助核算项目不能为空") + private List auxiliaryItemIds; + } + + @Data + public static class Amounts { + + @Schema(description = "期初金额", example = "1000.00") + @DecimalMin(value = "0", message = "期初金额不能小于 0") + private BigDecimal openingAmount; + + @Schema(description = "期初数量", example = "10.0000") + @DecimalMin(value = "0", message = "期初数量不能小于 0") + private BigDecimal openingQuantity; + + @Schema(description = "本年累计借方金额", example = "100.00") + @DecimalMin(value = "0", message = "本年累计借方金额不能小于 0") + private BigDecimal yearDebitAmount; + + @Schema(description = "本年累计借方数量", example = "1.0000") + @DecimalMin(value = "0", message = "本年累计借方数量不能小于 0") + private BigDecimal yearDebitQuantity; + + @Schema(description = "本年累计贷方金额", example = "50.00") + @DecimalMin(value = "0", message = "本年累计贷方金额不能小于 0") + private BigDecimal yearCreditAmount; + + @Schema(description = "本年累计贷方数量", example = "1.0000") + @DecimalMin(value = "0", message = "本年累计贷方数量不能小于 0") + private BigDecimal yearCreditQuantity; + + @Schema(description = "年初金额", example = "950.00") + @DecimalMin(value = "0", message = "年初金额不能小于 0") + private BigDecimal yearOpeningAmount; + + @Schema(description = "年初数量", example = "10.0000") + @DecimalMin(value = "0", message = "年初数量不能小于 0") + private BigDecimal yearOpeningQuantity; + + @Schema(description = "实际损益发生额", example = "0.00") + @DecimalMin(value = "0", message = "实际损益发生额不能小于 0") + private BigDecimal profitLossAmount; + + @Schema(description = "实际损益发生数量", example = "1.0000") + @DecimalMin(value = "0", message = "实际损益发生数量不能小于 0") + private BigDecimal profitLossQuantity; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsTrialBalanceRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsTrialBalanceRespVO.java new file mode 100644 index 0000000..94a1689 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsTrialBalanceRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 试算平衡 Response VO") +@Data +public class FmsTrialBalanceRespVO { + + @Schema(description = "期初借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00") + private BigDecimal openingDebitAmount; + + @Schema(description = "期初贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00") + private BigDecimal openingCreditAmount; + + @Schema(description = "期初差额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00") + private BigDecimal openingDifferenceAmount; + + @Schema(description = "本年累计借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal yearDebitAmount; + + @Schema(description = "本年累计贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal yearCreditAmount; + + @Schema(description = "本年累计差额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00") + private BigDecimal yearDifferenceAmount; + + @Schema(description = "是否平衡", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean balanced; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectDeleteReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectDeleteReqVO.java new file mode 100644 index 0000000..34b84fc --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectDeleteReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * FMS 科目删除 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 科目删除 Request VO") +@Data +public class FmsSubjectDeleteReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "科目编号数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "科目编号数组不能为空") + private List ids; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectImportExcelVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectImportExcelVO.java new file mode 100644 index 0000000..07a8dcf --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectImportExcelVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import cn.idev.excel.annotation.ExcelProperty; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * FMS 科目 Excel 导入 VO + * + * @author 芋道源码 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class FmsSubjectImportExcelVO { + + @ExcelProperty("科目编码") + @NotBlank(message = "科目编码不能为空") + private String code; + + @ExcelProperty("科目名称") + @NotBlank(message = "科目名称不能为空") + private String name; + + @ExcelProperty("上级科目编码") + private String parentSubjectCode; + + @ExcelProperty("余额方向") + @NotBlank(message = "余额方向不能为空") + private String balanceDirection; + + @ExcelProperty("类别") + @NotBlank(message = "类别不能为空") + private String categoryName; + + @ExcelProperty("辅助核算") + private String auxiliaryNames; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectImportRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectImportRespVO.java new file mode 100644 index 0000000..f1691a8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectImportRespVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - FMS 科目导入结果 Response VO") +@Data +public class FmsSubjectImportRespVO { + + @Schema(description = "总数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer totalCount; + + @Schema(description = "成功科目编码数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List successSubjectCodes; + + @Schema(description = "失败原因 Map", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureReasons; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectListReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectListReqVO.java new file mode 100644 index 0000000..cd84baa --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectListReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * FMS 科目列表 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 科目列表 Request VO") +@Data +public class FmsSubjectListReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "科目类型", example = "1") + @InEnum(FmsSubjectTypeEnum.class) + private Integer type; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectRespVO.java new file mode 100644 index 0000000..9a4bf25 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectRespVO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.framework.excel.core.convert.StringListConvert; +import cn.iocoder.yudao.module.fms.enums.DictTypeConstants; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.module.infra.enums.DictTypeConstants.BOOLEAN_STRING; + +/** + * FMS 科目 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 科目 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsSubjectRespVO { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountSetId; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @ExcelProperty(value = "编码", index = 0) + private String code; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金") + @ExcelProperty(value = "名称", index = 1) + private String name; + + @Schema(description = "上级科目编号", example = "1024") + private Long parentId; + + @Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "科目类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer category; + + @JsonIgnore // 仅 excel 导出使用 + @ExcelProperty(value = "类别", index = 2, converter = DictConvert.class) + @DictFormat(DictTypeConstants.SUBJECT_CATEGORY) + private String categoryDictValue; + + @Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "余额方向", index = 3, converter = DictConvert.class) + @DictFormat(DictTypeConstants.DEBIT_CREDIT_DIRECTION) + private Integer balanceDirection; + + @Schema(description = "辅助核算类别编号数组") + private List auxiliaryTypeIds; + + @Schema(description = "外币核算币别编号数组") + private List currencyIds; + + @Schema(description = "是否启用数量核算", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean quantityAccounting; + + @Schema(description = "数量单位", example = "件") + @ExcelProperty(value = "数量", index = 5) + private String quantityUnit; + + @Schema(description = "是否现金及现金等价物", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "是否现金项", index = 6, converter = DictConvert.class) + @DictFormat(BOOLEAN_STRING) + private Boolean cash; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer level; + + @Schema(description = "辅助核算类别名称数组") + @ExcelProperty(value = "辅助核算", index = 4, converter = StringListConvert.class) + private List auxiliaryTypeNames; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectSaveReqVO.java new file mode 100644 index 0000000..9ce9779 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectSaveReqVO.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectCategoryEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +/** + * FMS 科目保存 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 科目保存 Request VO") +@Data +public class FmsSubjectSaveReqVO { + + @Schema(description = "科目编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @NotBlank(message = "科目编码不能为空") + @Size(max = 64, message = "科目编码长度不能超过 64 个字符") + private String code; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金") + @NotBlank(message = "科目名称不能为空") + @Size(max = 255, message = "科目名称长度不能超过 255 个字符") + private String name; + + @Schema(description = "上级科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "上级科目编号不能为空") + private Long parentId; + + @Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "科目类型不能为空") + @InEnum(FmsSubjectTypeEnum.class) + private Integer type; + + @Schema(description = "科目类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "科目类别不能为空") + private Integer category; + + @Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "余额方向不能为空") + @InEnum(FmsDebitCreditDirectionEnum.class) + private Integer balanceDirection; + + @Schema(description = "辅助核算类别编号数组") + @NotNull(message = "辅助核算类别编号数组不能为空") + private List auxiliaryTypeIds; + + @Schema(description = "外币核算币别编号数组") + @NotNull(message = "外币核算币别编号数组不能为空") + private List currencyIds; + + @Schema(description = "是否启用数量核算", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "是否启用数量核算不能为空") + private Boolean quantityAccounting; + + @Schema(description = "数量单位", example = "件") + @Size(max = 255, message = "数量单位长度不能超过 255 个字符") + private String quantityUnit; + + @Schema(description = "是否现金及现金等价物", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "是否现金及现金等价物不能为空") + private Boolean cash; + + @Schema(description = "是否迁移上级科目历史数据") + private Boolean migrateParentData; + + @Schema(description = "辅助核算历史数据迁移项目数组") + private List auxiliaryMappings; + + @AssertTrue(message = "科目类别与科目类型不匹配") + public boolean isCategoryValid() { + return type == null || category == null || FmsSubjectCategoryEnum.valueOf(type, category) != null; + } + + @AssertTrue(message = "启用数量核算时数量单位不能为空") + public boolean isQuantityUnitValid() { + return !Boolean.TRUE.equals(quantityAccounting) || StrUtil.isNotBlank(quantityUnit); + } + + /** + * 辅助核算历史数据迁移项目 + */ + @Data + public static class AuxiliaryMapping { + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "辅助核算类别编号不能为空") + private Long typeId; + + @Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "辅助核算项目编号不能为空") + private Long itemId; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectStatusReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectStatusReqVO.java new file mode 100644 index 0000000..80d8685 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectStatusReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * FMS 科目状态更新 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 科目状态更新 Request VO") +@Data +public class FmsSubjectStatusReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "科目编号数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "科目编号数组不能为空") + private List ids; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectUsageRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectUsageRespVO.java new file mode 100644 index 0000000..1d0939f --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectUsageRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * FMS 科目使用情况 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 科目使用情况 Response VO") +@Data +public class FmsSubjectUsageRespVO { + + @Schema(description = "下级科目数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Long childCount; + + @Schema(description = "凭证分录数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long voucherEntryCount; + + @Schema(description = "初始余额数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long initialBalanceCount; + + @Schema(description = "辅助核算组合数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Long auxiliaryCombinationCount; + + @Schema(description = "包含数量数据的记录数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long quantityDataCount; + + @Schema(description = "是否已被业务使用", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean used; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateEntryVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateEntryVO.java new file mode 100644 index 0000000..45c3751 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateEntryVO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证模板分录 VO") +@Data +public class FmsVoucherTemplateEntryVO { + + @Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "摘要不能为空") + @Size(max = 500, message = "摘要长度不能超过 500 个字符") + private String digest; + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "科目不能为空") + private Long subjectId; + + @Schema(description = "数量") + @DecimalMin(value = "0", message = "数量不能小于 0") + @Digits(integer = 14, fraction = 4, message = "数量最多保留 4 位小数") + private BigDecimal quantity; + + @Schema(description = "单价") + @DecimalMin(value = "0", message = "单价不能小于 0") + @Digits(integer = 12, fraction = 6, message = "单价最多保留 6 位小数") + private BigDecimal unitPrice; + + @Schema(description = "借方金额") + @Digits(integer = 16, fraction = 2, message = "借方金额最多保留 2 位小数") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额") + @Digits(integer = 16, fraction = 2, message = "贷方金额最多保留 2 位小数") + private BigDecimal creditAmount; + + @Schema(description = "辅助核算项目数组") + @Valid + private List auxiliaries; + + @Schema(description = "管理后台 - FMS 凭证模板分录辅助核算项目") + @Data + public static class AuxiliaryItem { + + @Schema(description = "辅助核算类型") + private Integer type; + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "辅助核算类别不能为空") + private Long typeId; + + @Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "辅助核算项目不能为空") + private Long itemId; + + @Schema(description = "辅助核算项目名称") + private String name; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateRespVO.java new file mode 100644 index 0000000..736c8c4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证模板 Response VO") +@Data +public class FmsVoucherTemplateRespVO { + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountSetId; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long categoryId; + + @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String categoryName; + + @Schema(description = "凭证模板分录数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List entries; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateSaveReqVO.java new file mode 100644 index 0000000..1c53561 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplate/FmsVoucherTemplateSaveReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证模板保存 Request VO") +@Data +public class FmsVoucherTemplateSaveReqVO { + + @Schema(description = "模板编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "办公用品采购") + @NotBlank(message = "模板名称不能为空") + @Size(max = 255, message = "模板名称长度不能超过 255 个字符") + private String name; + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "模板分类不能为空") + private Long categoryId; + + @Schema(description = "凭证模板分录数组", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "凭证模板分录不能为空") + @Size(min = 2, message = "凭证模板至少需要两条分录") + private List entries; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplatecategory/FmsVoucherTemplateCategoryRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplatecategory/FmsVoucherTemplateCategoryRespVO.java new file mode 100644 index 0000000..c82bac6 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplatecategory/FmsVoucherTemplateCategoryRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 凭证模板分类 Response VO") +@Data +public class FmsVoucherTemplateCategoryRespVO { + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountSetId; + + @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "日常收支") + private String name; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplatecategory/FmsVoucherTemplateCategorySaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplatecategory/FmsVoucherTemplateCategorySaveReqVO.java new file mode 100644 index 0000000..73777fd --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/vouchertemplatecategory/FmsVoucherTemplateCategorySaveReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 凭证模板分类保存 Request VO") +@Data +public class FmsVoucherTemplateCategorySaveReqVO { + + @Schema(description = "分类编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "日常收支") + @NotBlank(message = "分类名称不能为空") + @Size(max = 255, message = "分类名称长度不能超过 255 个字符") + private String name; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/voucherword/FmsVoucherWordRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/voucherword/FmsVoucherWordRespVO.java new file mode 100644 index 0000000..49fcad9 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/voucherword/FmsVoucherWordRespVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * FMS 凭证字 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 凭证字 Response VO") +@Data +public class FmsVoucherWordRespVO { + + @Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "凭证字", requiredMode = Schema.RequiredMode.REQUIRED, example = "记") + private String name; + + @Schema(description = "打印标题", example = "记账凭证") + private String printTitle; + + @Schema(description = "是否默认凭证字", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean defaultStatus; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer sort; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountSetId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/voucherword/FmsVoucherWordSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/voucherword/FmsVoucherWordSaveReqVO.java new file mode 100644 index 0000000..1753ddf --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/voucherword/FmsVoucherWordSaveReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * FMS 凭证字保存 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - FMS 凭证字保存 Request VO") +@Data +public class FmsVoucherWordSaveReqVO { + + @Schema(description = "凭证字编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "凭证字", requiredMode = Schema.RequiredMode.REQUIRED, example = "记") + @NotBlank(message = "凭证字不能为空") + @Size(max = 255, message = "凭证字长度不能超过 255 个字符") + private String name; + + @Schema(description = "打印标题", example = "记账凭证") + @Size(max = 255, message = "打印标题长度不能超过 255 个字符") + private String printTitle; + + @Schema(description = "是否默认凭证字", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "是否默认凭证字不能为空") + private Boolean defaultStatus; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/FmsHomeController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/FmsHomeController.java new file mode 100644 index 0000000..45322b5 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/FmsHomeController.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.fms.controller.admin.home; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeMetricDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeRespVO; +import cn.iocoder.yudao.module.fms.service.home.FmsHomeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 首页") +@RestController +@RequestMapping("/fms/home") +@Validated +public class FmsHomeController { + + @Resource + private FmsHomeService homeService; + + @GetMapping("/get") + @Operation(summary = "获得首页") + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('fms:home:query')") + public CommonResult getHome(@RequestParam("accountSetId") @NotNull Long accountSetId) { + return success(homeService.getHome(accountSetId, getLoginUserId())); + } + + @GetMapping("/metric-detail") + @Operation(summary = "获得财务指标明细") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"), + @Parameter(name = "metricKey", description = "财务指标标识", required = true, example = "income") + }) + @PreAuthorize("@ss.hasPermission('fms:home:query')") + public CommonResult getMetricDetail( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("metricKey") @NotBlank String metricKey) { + return success(homeService.getMetricDetail(accountSetId, metricKey, getLoginUserId())); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/vo/FmsHomeMetricDetailRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/vo/FmsHomeMetricDetailRespVO.java new file mode 100644 index 0000000..fdc7e11 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/vo/FmsHomeMetricDetailRespVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.fms.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 首页财务指标明细 Response VO") +@Data +public class FmsHomeMetricDetailRespVO { + + @Schema(description = "指标标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "income") + private String key; + + @Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "收入") + private String name; + + @Schema(description = "财务指标趋势数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List trends; + + @Schema(description = "当期科目构成数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List structure; + + @Schema(description = "管理后台 - FMS 首页财务指标趋势 Response VO") + @Data + public static class Trend { + + @Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + private String month; + + @Schema(description = "指标金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00") + private BigDecimal amount; + + } + + @Schema(description = "管理后台 - FMS 首页财务指标科目构成 Response VO") + @Data + public static class Structure { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001") + private String subjectCode; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "主营业务收入") + private String subjectName; + + @Schema(description = "科目金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000.00") + private BigDecimal amount; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/vo/FmsHomeRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/vo/FmsHomeRespVO.java new file mode 100644 index 0000000..fc0a97c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/home/vo/FmsHomeRespVO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.fms.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 首页 Response VO") +@Data +public class FmsHomeRespVO { + + @Schema(description = "当前会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + private String currentMonth; + + @Schema(description = "当期财务指标数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List metrics; + + @Schema(description = "财务指标趋势数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List trends; + + @Schema(description = "管理后台 - FMS 首页财务指标 Response VO") + @Data + public static class Metric { + + @Schema(description = "指标标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "income") + private String key; + + @Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "收入") + private String name; + + @Schema(description = "指标金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00") + private BigDecimal amount; + + } + + @Schema(description = "管理后台 - FMS 首页财务指标趋势 Response VO") + @Data + public static class Trend { + + @Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + private String month; + + @Schema(description = "动态财务指标数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List metrics; + + @Schema(description = "收入", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal income; + + @Schema(description = "成本", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal operatingCost; + + @Schema(description = "利润", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal profit; + + @Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal expense; + + @Schema(description = "其他", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal other; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerController.java new file mode 100644 index 0000000..5b606e4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerController.java @@ -0,0 +1,236 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerAuxiliaryListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarybalance.FmsLedgerAuxiliaryBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarydetail.FmsLedgerAuxiliaryDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailExportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.general.FmsLedgerGeneralRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.multicolumn.FmsLedgerMultiColumnRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitydetail.FmsLedgerQuantityDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitygeneral.FmsLedgerQuantityGeneralRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 账簿") +@RestController +@RequestMapping("/fms/ledger") +@Validated +public class FmsLedgerController { + + @Resource + private FmsLedgerService ledgerService; + + // ==================== 明细账 ==================== + + @GetMapping("/detail/list") + @Operation(summary = "获得明细账") + @PreAuthorize("@ss.hasPermission('fms:ledger:detail:query')") + public CommonResult> getDetailList(@Valid FmsLedgerListReqVO listReqVO) { + return success(ledgerService.getDetailList(listReqVO, getLoginUserId())); + } + + @GetMapping("/detail/subject-list") + @Operation(summary = "获得指定期间有发生额的明细账科目") + public CommonResult> getDetailSubjectList(@Valid FmsLedgerListReqVO listReqVO) { + // TODO @AI:先查询出来,list 变量;然后在去 convert;字段不多,倒是可以考虑 beanutils 去 copy 下? + return success(convertList(ledgerService.getDetailSubjectList(listReqVO, getLoginUserId()), subject -> + new FmsSubjectRespVO().setId(subject.getId()).setParentId(subject.getParentId()) + .setCode(subject.getCode()).setName(subject.getName()).setType(subject.getType()) + .setBalanceDirection(subject.getBalanceDirection()).setStatus(subject.getStatus()) + .setLevel(subject.getLevel()).setAuxiliaryTypeIds(subject.getAuxiliaryTypeIds()) + .setQuantityAccounting(subject.getQuantityAccounting()).setQuantityUnit(subject.getQuantityUnit()))); + } + + @GetMapping("/detail/export-excel") + @Operation(summary = "导出明细账") + @PreAuthorize("@ss.hasPermission('fms:ledger:detail:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportDetail(@Valid FmsLedgerListReqVO listReqVO, HttpServletResponse response) throws IOException { + List rows = convertList( + ledgerService.getDetailList(listReqVO, getLoginUserId()), item -> + BeanUtils.toBean(item, FmsLedgerDetailExportRespVO.class) + .setSubject(item.getSubjectCode() + " " + item.getSubjectName())); + ExcelUtils.write(response, "明细账.xls", "明细账", FmsLedgerDetailExportRespVO.class, rows); + } + + // ==================== 总账 ==================== + + @GetMapping("/general/list") + @Operation(summary = "获得总账") + @PreAuthorize("@ss.hasPermission('fms:ledger:general:query')") + public CommonResult> getGeneralList(@Valid FmsLedgerListReqVO listReqVO) { + return success(ledgerService.getGeneralList(listReqVO, getLoginUserId())); + } + + @GetMapping("/general/export-excel") + @Operation(summary = "导出总账") + @PreAuthorize("@ss.hasPermission('fms:ledger:general:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportGeneral(@Valid FmsLedgerListReqVO listReqVO, HttpServletResponse response) throws IOException { + ExcelUtils.write(response, "总账.xls", "总账", FmsLedgerGeneralRespVO.class, + ledgerService.getGeneralList(listReqVO, getLoginUserId())); + } + + // ==================== 科目余额表 ==================== + + @GetMapping("/subject-balance/list") + @Operation(summary = "获得科目余额表") + @PreAuthorize("@ss.hasPermission('fms:ledger:subject-balance:query')") + public CommonResult> getSubjectBalanceList(@Valid FmsLedgerListReqVO listReqVO) { + return success(ledgerService.getSubjectBalanceList(listReqVO, getLoginUserId())); + } + + @GetMapping("/subject-balance/export-excel") + @Operation(summary = "导出科目余额表") + @PreAuthorize("@ss.hasPermission('fms:ledger:subject-balance:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportSubjectBalance(@Valid FmsLedgerListReqVO listReqVO, HttpServletResponse response) throws IOException { + ExcelUtils.write(response, "科目余额表.xls", "科目余额表", FmsLedgerSubjectBalanceRespVO.class, + flattenSubjectBalanceList(ledgerService.getSubjectBalanceList(listReqVO, getLoginUserId()))); + } + + // ==================== 多栏账 ==================== + + @GetMapping("/multi-column/list") + @Operation(summary = "获得多栏账") + @PreAuthorize("@ss.hasPermission('fms:ledger:multi-column:query')") + public CommonResult getMultiColumn(@Valid FmsLedgerListReqVO listReqVO) { + return success(ledgerService.getMultiColumn(listReqVO, getLoginUserId())); + } + + @GetMapping("/multi-column/export-excel") + @Operation(summary = "导出多栏账") + @PreAuthorize("@ss.hasPermission('fms:ledger:multi-column:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportMultiColumn(@Valid FmsLedgerListReqVO listReqVO, HttpServletResponse response) throws IOException { + FmsLedgerMultiColumnRespVO result = ledgerService.getMultiColumn(listReqVO, getLoginUserId()); + ExcelUtils.write(response, "多栏账.xls", "多栏账", + FmsLedgerMultiColumnExcelHelper.buildHead(result), + FmsLedgerMultiColumnExcelHelper.buildData(result)); + } + + // ==================== 核算项目明细账 ==================== + + @GetMapping("/auxiliary-detail/list") + @Operation(summary = "获得核算项目明细账") + @PreAuthorize("@ss.hasPermission('fms:ledger:detail:query')") + public CommonResult> getAuxiliaryDetailList(@Valid FmsLedgerAuxiliaryListReqVO listReqVO) { + return success(ledgerService.getAuxiliaryDetailList(listReqVO, getLoginUserId())); + } + + @GetMapping("/auxiliary-detail/export-excel") + @Operation(summary = "导出核算项目明细账") + @PreAuthorize("@ss.hasPermission('fms:ledger:detail:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportAuxiliaryDetail(@Valid FmsLedgerAuxiliaryListReqVO listReqVO, HttpServletResponse response) throws IOException { + ExcelUtils.write(response, "核算项目明细账.xls", "核算项目明细账", + FmsLedgerAuxiliaryDetailRespVO.class, + ledgerService.getAuxiliaryDetailList(listReqVO, getLoginUserId())); + } + + // ==================== 核算项目余额表 ==================== + + @GetMapping("/auxiliary-balance/list") + @Operation(summary = "获得核算项目余额表") + @PreAuthorize("@ss.hasPermission('fms:ledger:subject-balance:query')") + public CommonResult> getAuxiliaryBalanceList(@Valid FmsLedgerAuxiliaryListReqVO listReqVO) { + return success(ledgerService.getAuxiliaryBalanceList(listReqVO, getLoginUserId())); + } + + @GetMapping("/auxiliary-balance/export-excel") + @Operation(summary = "导出核算项目余额表") + @PreAuthorize("@ss.hasPermission('fms:ledger:subject-balance:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportAuxiliaryBalance(@Valid FmsLedgerAuxiliaryListReqVO listReqVO, HttpServletResponse response) throws IOException { + ExcelUtils.write(response, "核算项目余额表.xls", "核算项目余额表", + FmsLedgerAuxiliaryBalanceRespVO.class, + ledgerService.getAuxiliaryBalanceList(listReqVO, getLoginUserId())); + } + + // ==================== 数量金额明细账 ==================== + + @GetMapping("/quantity-detail/list") + @Operation(summary = "获得数量金额明细账") + @PreAuthorize("@ss.hasPermission('fms:ledger:detail:query')") + public CommonResult> getQuantityDetailList(@Valid FmsLedgerListReqVO listReqVO) { + return success(ledgerService.getQuantityDetailList(listReqVO, getLoginUserId())); + } + + @GetMapping("/quantity-detail/export-excel") + @Operation(summary = "导出数量金额明细账") + @PreAuthorize("@ss.hasPermission('fms:ledger:detail:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportQuantityDetail(@Valid FmsLedgerListReqVO listReqVO, HttpServletResponse response) throws IOException { + ExcelUtils.write(response, "数量金额明细账.xls", "数量金额明细账", + FmsLedgerQuantityDetailRespVO.class, + ledgerService.getQuantityDetailList(listReqVO, getLoginUserId())); + } + + // ==================== 数量金额总账 ==================== + + @GetMapping("/quantity-general/list") + @Operation(summary = "获得数量金额总账") + @PreAuthorize("@ss.hasPermission('fms:ledger:general:query')") + public CommonResult> getQuantityGeneralList(@Valid FmsLedgerListReqVO listReqVO) { + return success(ledgerService.getQuantityGeneralList(listReqVO, getLoginUserId())); + } + + @GetMapping("/quantity-general/export-excel") + @Operation(summary = "导出数量金额总账") + @PreAuthorize("@ss.hasPermission('fms:ledger:general:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportQuantityGeneral(@Valid FmsLedgerListReqVO listReqVO, HttpServletResponse response) throws IOException { + ExcelUtils.write(response, "数量金额总账.xls", "数量金额总账", + FmsLedgerQuantityGeneralRespVO.class, + flattenQuantityGeneralList(ledgerService.getQuantityGeneralList( + listReqVO, getLoginUserId()))); + } + + private static List flattenSubjectBalanceList(List list) { + List result = new ArrayList<>(); + list.forEach(item -> { + result.add(item); + result.addAll(flattenSubjectBalanceList(item.getChildren())); + }); + return result; + } + + private static List flattenQuantityGeneralList(List list) { + List result = new ArrayList<>(); + list.forEach(item -> { + if (Boolean.TRUE.equals(item.getQuantityAccounting()) || CollUtil.isNotEmpty(item.getChildren())) { + result.add(item); + } + result.addAll(flattenQuantityGeneralList(item.getChildren())); + }); + return result; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerMultiColumnExcelHelper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerMultiColumnExcelHelper.java new file mode 100644 index 0000000..79ec67b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerMultiColumnExcelHelper.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.multicolumn.FmsLedgerMultiColumnRespVO; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; + +/** + * FMS 多栏账 Excel 构建工具 + * + * @author 芋道源码 + */ +public class FmsLedgerMultiColumnExcelHelper { + + private FmsLedgerMultiColumnExcelHelper() { + } + + /** + * 构建多栏账动态表头 + * + * @param result 多栏账结果 + * @return 动态表头 + */ + public static List> buildHead(FmsLedgerMultiColumnRespVO result) { + List> head = new ArrayList<>(); + head.add(Arrays.asList("日期", "日期")); + head.add(Arrays.asList("凭证字号", "凭证字号")); + head.add(Arrays.asList("摘要", "摘要")); + head.add(Arrays.asList("发生额", "借方")); + head.add(Arrays.asList("发生额", "贷方")); + head.add(Arrays.asList("余额", "方向")); + head.add(Arrays.asList("余额", "金额")); + for (FmsLedgerMultiColumnRespVO.Column column : getColumns(result, + FmsDebitCreditDirectionEnum.DEBIT.getType())) { + head.add(Arrays.asList(FmsDebitCreditDirectionEnum.DEBIT.getName(), + column.getSubjectCode() + "/" + column.getSubjectName())); + } + for (FmsLedgerMultiColumnRespVO.Column column : getColumns(result, + FmsDebitCreditDirectionEnum.CREDIT.getType())) { + head.add(Arrays.asList(FmsDebitCreditDirectionEnum.CREDIT.getName(), + column.getSubjectCode() + "/" + column.getSubjectName())); + } + return head; + } + + /** + * 构建多栏账动态数据 + * + * @param result 多栏账结果 + * @return 动态数据 + */ + public static List> buildData(FmsLedgerMultiColumnRespVO result) { + List debitColumns = getColumns( + result, FmsDebitCreditDirectionEnum.DEBIT.getType()); + List creditColumns = getColumns( + result, FmsDebitCreditDirectionEnum.CREDIT.getType()); + List> data = new ArrayList<>(); + for (FmsLedgerDetailRespVO row : CollUtil.emptyIfNull(result.getRows())) { + List values = new ArrayList<>(Arrays.asList(row.getAccountDate(), row.getVoucherNumber(), + row.getDigest(), row.getDebitAmount(), row.getCreditAmount(), + row.getBalanceDirection(), row.getBalance())); + debitColumns.forEach(column -> values.add(row.getColumnAmounts().get(column.getSubjectId()))); + creditColumns.forEach(column -> values.add(row.getColumnAmounts().get(column.getSubjectId()))); + data.add(values); + } + return data; + } + + private static List getColumns( + FmsLedgerMultiColumnRespVO result, Integer balanceDirection) { + return filterList(CollUtil.emptyIfNull(result.getColumns()), + column -> balanceDirection.equals(column.getBalanceDirection())); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerAuxiliaryListReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerAuxiliaryListReqVO.java new file mode 100644 index 0000000..7c7305b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerAuxiliaryListReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 核算项目账簿列表查询 Request VO") +@Data +public class FmsLedgerAuxiliaryListReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "开始会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotNull(message = "开始会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "开始会计期间格式不正确") + private String startMonth; + + @Schema(description = "结束会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-12") + @NotNull(message = "结束会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "结束会计期间格式不正确") + private String endMonth; + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "辅助核算类别编号不能为空") + private Long auxiliaryTypeId; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "辅助核算项目编号", example = "1024") + private Long auxiliaryItemId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerEntryVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerEntryVO.java new file mode 100644 index 0000000..15c7206 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerEntryVO.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo; + +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * FMS 账簿凭证分录 VO + * + * @author 芋道源码 + */ +@Data +public class FmsLedgerEntryVO { + + /** + * 分录编号 + */ + private Long entryId; + /** + * 凭证编号 + */ + private Long voucherId; + /** + * 科目编号 + */ + private Long subjectId; + /** + * 凭证日期 + */ + private LocalDateTime voucherTime; + /** + * 凭证号 + */ + private Integer voucherNumber; + /** + * 凭证字名称 + */ + private String voucherWordName; + /** + * 摘要内容 + */ + private String digest; + /** + * 借方金额 + */ + private BigDecimal debitAmount; + /** + * 贷方金额 + */ + private BigDecimal creditAmount; + /** + * 数量 + */ + private BigDecimal quantity; + /** + * 单价 + */ + private BigDecimal unitPrice; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 辅助核算组合编号 + */ + private Long assistCombinationId; + /** + * 辅助核算项目数组 + */ + private List auxiliaries; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerListReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerListReqVO.java new file mode 100644 index 0000000..8de281c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/FmsLedgerListReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 账簿列表查询 Request VO") +@Data +public class FmsLedgerListReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "开始会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotNull(message = "开始会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "开始会计期间格式不正确") + private String startMonth; + + @Schema(description = "结束会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-12") + @NotNull(message = "结束会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "结束会计期间格式不正确") + private String endMonth; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "起始科目编号", example = "1024") + private Long startSubjectId; + + @Schema(description = "结束科目编号", example = "2048") + private Long endSubjectId; + + @Schema(description = "最小科目级次", example = "1") + @Min(value = 1, message = "最小科目级次不能小于 1") + @Max(value = 10, message = "最小科目级次不能大于 10") + private Integer minLevel; + + @Schema(description = "最大科目级次", example = "4") + @Min(value = 1, message = "最大科目级次不能小于 1") + @Max(value = 10, message = "最大科目级次不能大于 10") + private Integer maxLevel; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarybalance/FmsLedgerAuxiliaryBalanceRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarybalance/FmsLedgerAuxiliaryBalanceRespVO.java new file mode 100644 index 0000000..c4f53e0 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarybalance/FmsLedgerAuxiliaryBalanceRespVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarybalance; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 核算项目余额 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsLedgerAuxiliaryBalanceRespVO { + + @Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long auxiliaryItemId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("编码") + private String code; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("项目名称") + private String name; + + @Schema(description = "期初借方余额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("期初借方") + private BigDecimal openingDebitAmount; + + @Schema(description = "期初贷方余额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("期初贷方") + private BigDecimal openingCreditAmount; + + @Schema(description = "本期借方发生额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("本期借方") + private BigDecimal periodDebitAmount; + + @Schema(description = "本期贷方发生额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("本期贷方") + private BigDecimal periodCreditAmount; + + @Schema(description = "本年累计借方发生额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("本年累计借方") + private BigDecimal yearDebitAmount; + + @Schema(description = "本年累计贷方发生额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("本年累计贷方") + private BigDecimal yearCreditAmount; + + @Schema(description = "期末借方余额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("期末借方") + private BigDecimal endingDebitAmount; + + @Schema(description = "期末贷方余额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("期末贷方") + private BigDecimal endingCreditAmount; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarydetail/FmsLedgerAuxiliaryDetailRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarydetail/FmsLedgerAuxiliaryDetailRespVO.java new file mode 100644 index 0000000..5b8aee3 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarydetail/FmsLedgerAuxiliaryDetailRespVO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarydetail; + +import com.fasterxml.jackson.annotation.JsonFormat; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Schema(description = "管理后台 - FMS 核算项目明细账 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsLedgerAuxiliaryDetailRespVO { + + public static final Integer ROW_TYPE_OPENING = 1; + public static final Integer ROW_TYPE_VOUCHER = 2; + public static final Integer ROW_TYPE_PERIOD_TOTAL = 3; + public static final Integer ROW_TYPE_YEAR_TOTAL = 4; + + @Schema(description = "行类型", example = "2") + private Integer rowType; + + @Schema(description = "分录编号", example = "1024") + private Long entryId; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", example = "1001") + private String subjectCode; + + @Schema(description = "科目名称", example = "库存现金") + private String subjectName; + + @Schema(description = "会计期间", example = "2026-08") + private String period; + + @Schema(description = "日期") + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty("日期") + private LocalDate accountDate; + + @Schema(description = "凭证编号", example = "1024") + private Long voucherId; + + @Schema(description = "凭证字号", example = "记-1") + @ExcelProperty("凭证字号") + private String voucherNumber; + + @Schema(description = "摘要", example = "报销办公用品") + @ExcelProperty("摘要") + private String digest; + + @Schema(description = "借方金额") + @ExcelProperty("借方") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额") + @ExcelProperty("贷方") + private BigDecimal creditAmount; + + @Schema(description = "余额方向", example = "借") + @ExcelProperty("方向") + private String balanceDirection; + + @Schema(description = "余额") + @ExcelProperty("余额") + private BigDecimal balance; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailExportRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailExportRespVO.java new file mode 100644 index 0000000..3dd6606 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailExportRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * FMS 明细账导出 Response VO + * + * @author 芋道源码 + */ +@Data +@ExcelIgnoreUnannotated +public class FmsLedgerDetailExportRespVO { + + @ExcelProperty("科目") + private String subject; + + @ExcelProperty("日期") + private LocalDate accountDate; + + @ExcelProperty("凭证字号") + private String voucherNumber; + + @ExcelProperty("摘要") + private String digest; + + @ExcelProperty("借方") + private BigDecimal debitAmount; + + @ExcelProperty("贷方") + private BigDecimal creditAmount; + + @ExcelProperty("方向") + private String balanceDirection; + + @ExcelProperty("余额") + private BigDecimal balance; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailRespVO.java new file mode 100644 index 0000000..a0f67ec --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailRespVO.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail; + +import com.fasterxml.jackson.annotation.JsonFormat; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Schema(description = "管理后台 - FMS 账簿明细 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsLedgerDetailRespVO { + + public static final Integer ROW_TYPE_OPENING = 1; + public static final Integer ROW_TYPE_VOUCHER = 2; + public static final Integer ROW_TYPE_PERIOD_TOTAL = 3; + public static final Integer ROW_TYPE_YEAR_TOTAL = 4; + public static final Integer ROW_TYPE_ENDING = 5; + + @Schema(description = "行类型", example = "2") + private Integer rowType; + + @Schema(description = "分录编号", example = "1024") + private Long entryId; + + @Schema(description = "分录科目编号", example = "1024") + private Long entrySubjectId; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", example = "1001") + private String subjectCode; + + @Schema(description = "科目名称", example = "库存现金") + private String subjectName; + + @Schema(description = "会计期间", example = "2026-08") + private String period; + + @Schema(description = "日期") + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty("日期") + private LocalDate accountDate; + + @Schema(description = "凭证编号", example = "1024") + private Long voucherId; + + @Schema(description = "凭证字号", example = "记-1") + @ExcelProperty("凭证字号") + private String voucherNumber; + + @Schema(description = "摘要", example = "报销办公用品") + @ExcelProperty("摘要") + private String digest; + + @Schema(description = "借方金额") + @ExcelProperty("借方") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额") + @ExcelProperty("贷方") + private BigDecimal creditAmount; + + @Schema(description = "余额方向", example = "借") + @ExcelProperty("方向") + private String balanceDirection; + + @Schema(description = "余额") + @ExcelProperty("余额") + private BigDecimal balance; + + @Schema(description = "借方数量") + private BigDecimal debitQuantity; + + @Schema(description = "贷方数量") + private BigDecimal creditQuantity; + + @Schema(description = "结存数量") + private BigDecimal balanceQuantity; + + @Schema(description = "单价") + private BigDecimal unitPrice; + + @Schema(description = "计量单位", example = "件") + private String quantityUnit; + + @Schema(description = "多栏账科目金额") + private java.util.Map columnAmounts; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/general/FmsLedgerGeneralRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/general/FmsLedgerGeneralRespVO.java new file mode 100644 index 0000000..12afe27 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/general/FmsLedgerGeneralRespVO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.general; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 总账 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsLedgerGeneralRespVO { + + @Schema(description = "行类型", example = "2") + private Integer rowType; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", example = "1001") + @ExcelProperty("科目编码") + private String subjectCode; + + @Schema(description = "科目名称", example = "库存现金") + @ExcelProperty("科目名称") + private String subjectName; + + @Schema(description = "会计期间", example = "2026-08") + @ExcelProperty("期间") + private String period; + + @Schema(description = "摘要", example = "本期合计") + @ExcelProperty("摘要") + private String digest; + + @Schema(description = "借方金额") + @ExcelProperty("借方") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额") + @ExcelProperty("贷方") + private BigDecimal creditAmount; + + @Schema(description = "余额方向", example = "借") + @ExcelProperty("方向") + private String balanceDirection; + + @Schema(description = "余额") + @ExcelProperty("余额") + private BigDecimal balance; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/multicolumn/FmsLedgerMultiColumnRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/multicolumn/FmsLedgerMultiColumnRespVO.java new file mode 100644 index 0000000..e2af0d7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/multicolumn/FmsLedgerMultiColumnRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.multicolumn; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailRespVO; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 多栏账 Response VO") +@Data +@Accessors(chain = true) +public class FmsLedgerMultiColumnRespVO { + + @Schema(description = "动态科目列") + private List columns; + + @Schema(description = "账簿行") + private List rows; + + @Schema(description = "管理后台 - FMS 多栏账科目列 Response VO") + @Data + public static class Column { + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", example = "560101") + private String subjectCode; + + @Schema(description = "科目名称", example = "办公用品") + private String subjectName; + + @Schema(description = "余额方向", example = "1") + private Integer balanceDirection; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitydetail/FmsLedgerQuantityDetailRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitydetail/FmsLedgerQuantityDetailRespVO.java new file mode 100644 index 0000000..3317ccb --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitydetail/FmsLedgerQuantityDetailRespVO.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitydetail; + +import com.fasterxml.jackson.annotation.JsonFormat; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Schema(description = "管理后台 - FMS 数量金额明细账 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsLedgerQuantityDetailRespVO { + + public static final Integer ROW_TYPE_OPENING = 1; + public static final Integer ROW_TYPE_VOUCHER = 2; + public static final Integer ROW_TYPE_PERIOD_TOTAL = 3; + public static final Integer ROW_TYPE_YEAR_TOTAL = 4; + + @Schema(description = "行类型", example = "2") + private Integer rowType; + + @Schema(description = "分录编号", example = "1024") + private Long entryId; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", example = "1405") + private String subjectCode; + + @Schema(description = "科目名称", example = "库存商品") + private String subjectName; + + @Schema(description = "会计期间", example = "2026-08") + private String period; + + @Schema(description = "日期") + @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty("日期") + private LocalDate accountDate; + + @Schema(description = "凭证编号", example = "1024") + private Long voucherId; + + @Schema(description = "凭证字号", example = "记-1") + @ExcelProperty("凭证字号") + private String voucherNumber; + + @Schema(description = "摘要", example = "采购商品") + @ExcelProperty("摘要") + private String digest; + + @Schema(description = "借方金额") + @ExcelProperty("借方金额") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额") + @ExcelProperty("贷方金额") + private BigDecimal creditAmount; + + @Schema(description = "余额方向", example = "借") + @ExcelProperty("方向") + private String balanceDirection; + + @Schema(description = "余额") + @ExcelProperty("结存金额") + private BigDecimal balance; + + @Schema(description = "借方数量") + @ExcelProperty("借方数量") + private BigDecimal debitQuantity; + + @Schema(description = "贷方数量") + @ExcelProperty("贷方数量") + private BigDecimal creditQuantity; + + @Schema(description = "结存数量") + @ExcelProperty("结存数量") + private BigDecimal balanceQuantity; + + @Schema(description = "单价") + @ExcelProperty("分录单价") + private BigDecimal unitPrice; + + @ExcelProperty("期初单价") + private BigDecimal openingUnitPrice; + + @ExcelProperty("本期单价") + private BigDecimal periodUnitPrice; + + @ExcelProperty("累计单价") + private BigDecimal yearUnitPrice; + + @ExcelProperty("期末单价") + private BigDecimal endingUnitPrice; + + @Schema(description = "计量单位", example = "件") + @ExcelProperty("单位") + private String quantityUnit; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitygeneral/FmsLedgerQuantityGeneralRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitygeneral/FmsLedgerQuantityGeneralRespVO.java new file mode 100644 index 0000000..2f44668 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitygeneral/FmsLedgerQuantityGeneralRespVO.java @@ -0,0 +1,120 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitygeneral; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - FMS 数量金额总账 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsLedgerQuantityGeneralRespVO { + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", example = "1405") + @ExcelProperty({"科目", "编码"}) + private String subjectCode; + + @Schema(description = "科目名称", example = "库存商品") + @ExcelProperty({"科目", "名称"}) + private String subjectName; + + @Schema(description = "科目级次", example = "1") + private Integer level; + + @Schema(description = "是否启用数量核算") + private Boolean quantityAccounting; + + @Schema(description = "计量单位", example = "件") + @ExcelProperty({"基础信息", "单位"}) + private String quantityUnit; + + @Schema(description = "期初余额方向", example = "借") + @ExcelProperty({"期初余额", "方向"}) + private String openingBalanceDirection; + + @Schema(description = "期初数量") + @ExcelProperty({"期初余额", "数量"}) + private BigDecimal openingQuantity; + + @Schema(description = "期初单价") + @ExcelProperty({"期初余额", "单价"}) + private BigDecimal openingUnitPrice; + + @Schema(description = "期初借方余额") + @ExcelIgnore + private BigDecimal openingDebitAmount; + + @Schema(description = "期初贷方余额") + @ExcelIgnore + private BigDecimal openingCreditAmount; + + @ExcelProperty({"期初余额", "金额"}) + private BigDecimal openingAmount; + + @Schema(description = "本期借方数量") + @ExcelProperty({"本期借方", "数量"}) + private BigDecimal periodDebitQuantity; + + @Schema(description = "本期借方发生额") + @ExcelProperty({"本期借方", "金额"}) + private BigDecimal periodDebitAmount; + + @Schema(description = "本期贷方数量") + @ExcelProperty({"本期贷方", "数量"}) + private BigDecimal periodCreditQuantity; + + @Schema(description = "本期贷方发生额") + @ExcelProperty({"本期贷方", "金额"}) + private BigDecimal periodCreditAmount; + + @Schema(description = "本年累计借方数量") + @ExcelProperty({"本年累计借方", "数量"}) + private BigDecimal yearDebitQuantity; + + @Schema(description = "本年累计借方发生额") + @ExcelProperty({"本年累计借方", "金额"}) + private BigDecimal yearDebitAmount; + + @Schema(description = "本年累计贷方数量") + @ExcelProperty({"本年累计贷方", "数量"}) + private BigDecimal yearCreditQuantity; + + @Schema(description = "本年累计贷方发生额") + @ExcelProperty({"本年累计贷方", "金额"}) + private BigDecimal yearCreditAmount; + + @Schema(description = "期末余额方向", example = "借") + @ExcelProperty({"期末余额", "方向"}) + private String endingBalanceDirection; + + @Schema(description = "期末数量") + @ExcelProperty({"期末余额", "数量"}) + private BigDecimal endingQuantity; + + @Schema(description = "期末单价") + @ExcelProperty({"期末余额", "单价"}) + private BigDecimal endingUnitPrice; + + @Schema(description = "期末借方余额") + @ExcelIgnore + private BigDecimal endingDebitAmount; + + @Schema(description = "期末贷方余额") + @ExcelIgnore + private BigDecimal endingCreditAmount; + + @ExcelProperty({"期末余额", "金额"}) + private BigDecimal endingAmount; + + @Schema(description = "下级科目") + private List children = new ArrayList<>(); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/subjectbalance/FmsLedgerSubjectBalanceRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/subjectbalance/FmsLedgerSubjectBalanceRespVO.java new file mode 100644 index 0000000..5249b9d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/subjectbalance/FmsLedgerSubjectBalanceRespVO.java @@ -0,0 +1,116 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - FMS 科目余额 Response VO") +@Data +@Accessors(chain = true) +@ExcelIgnoreUnannotated +public class FmsLedgerSubjectBalanceRespVO { + + public static final int NODE_TYPE_SUBJECT = 1; + public static final int NODE_TYPE_AUXILIARY_COMBINATION = 2; + + @Schema(description = "节点唯一键", example = "S:1024") + private String nodeKey; + + @Schema(description = "节点类型", example = "1") + private Integer nodeType; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "辅助核算组合编号", example = "2048") + private Long assistCombinationId; + + @Schema(description = "科目编码", example = "1001") + @ExcelProperty("科目编码") + private String subjectCode; + + @Schema(description = "科目名称", example = "库存现金") + @ExcelProperty("科目名称") + private String subjectName; + + @Schema(description = "科目级次", example = "1") + private Integer level; + + @Schema(description = "是否启用数量核算") + private Boolean quantityAccounting; + + @Schema(description = "计量单位", example = "件") + private String quantityUnit; + + @Schema(description = "期初借方余额") + @ExcelProperty("期初借方") + private BigDecimal openingDebitAmount; + + @Schema(description = "期初贷方余额") + @ExcelProperty("期初贷方") + private BigDecimal openingCreditAmount; + + @Schema(description = "期初余额方向", example = "借") + private String openingBalanceDirection; + + @Schema(description = "期初数量") + private BigDecimal openingQuantity; + + @Schema(description = "期初单价") + private BigDecimal openingUnitPrice; + + @Schema(description = "本期借方发生额") + @ExcelProperty("本期借方") + private BigDecimal periodDebitAmount; + + @Schema(description = "本期贷方发生额") + @ExcelProperty("本期贷方") + private BigDecimal periodCreditAmount; + + @Schema(description = "本期借方数量") + private BigDecimal periodDebitQuantity; + + @Schema(description = "本期贷方数量") + private BigDecimal periodCreditQuantity; + + @Schema(description = "本年累计借方发生额") + @ExcelProperty("本年累计借方") + private BigDecimal yearDebitAmount; + + @Schema(description = "本年累计贷方发生额") + @ExcelProperty("本年累计贷方") + private BigDecimal yearCreditAmount; + + @Schema(description = "本年累计借方数量") + private BigDecimal yearDebitQuantity; + + @Schema(description = "本年累计贷方数量") + private BigDecimal yearCreditQuantity; + + @Schema(description = "期末借方余额") + @ExcelProperty("期末借方") + private BigDecimal endingDebitAmount; + + @Schema(description = "期末贷方余额") + @ExcelProperty("期末贷方") + private BigDecimal endingCreditAmount; + + @Schema(description = "期末余额方向", example = "借") + private String endingBalanceDirection; + + @Schema(description = "期末数量") + private BigDecimal endingQuantity; + + @Schema(description = "期末单价") + private BigDecimal endingUnitPrice; + + @Schema(description = "下级科目") + private List children = new ArrayList<>(); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsBalanceSheetController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsBalanceSheetController.java new file mode 100644 index 0000000..fc97b43 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsBalanceSheetController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.service.report.FmsBalanceSheetService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 资产负债表") +@RestController +@RequestMapping("/fms/report/balance-sheet") +@Validated +public class FmsBalanceSheetController { + + @Resource + private FmsBalanceSheetService balanceSheetService; + + @GetMapping("/get") + @Operation(summary = "获得资产负债表") + @PreAuthorize("@ss.hasPermission('fms:report:balance-sheet:query')") + public CommonResult> getBalanceSheet(@Valid FmsReportListReqVO listReqVO) { + return success(balanceSheetService.getBalanceSheet(listReqVO, getLoginUserId())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出资产负债表") + @PreAuthorize("@ss.hasPermission('fms:report:balance-sheet:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportBalanceSheet(@Valid FmsReportListReqVO listReqVO, + HttpServletResponse response) throws IOException { + List list = balanceSheetService.getBalanceSheet(listReqVO, getLoginUserId()); + ExcelUtils.write(response, "资产负债表.xls", "资产负债表", FmsBalanceSheetRowRespVO.class, list); + } + + @PutMapping("/update") + @Operation(summary = "更新资产负债表公式") + @PreAuthorize("@ss.hasPermission('fms:report:balance-sheet:update')") + public CommonResult updateBalanceSheetFormula(@Valid @RequestBody FmsReportFormulaUpdateReqVO updateReqVO) { + balanceSheetService.updateBalanceSheetFormula(updateReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/check") + @Operation(summary = "检查资产负债表") + @PreAuthorize("@ss.hasPermission('fms:report:balance-sheet:query')") + public CommonResult checkBalanceSheet(@Valid FmsReportListReqVO listReqVO) { + return success(balanceSheetService.checkBalanceSheet(listReqVO, getLoginUserId())); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsCashFlowStatementController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsCashFlowStatementController.java new file mode 100644 index 0000000..c8e0dc6 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsCashFlowStatementController.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowStatementUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.service.report.FmsCashFlowStatementService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 现金流量表") +@RestController +@RequestMapping("/fms/report/cash-flow-statement") +@Validated +public class FmsCashFlowStatementController { + + @Resource + private FmsCashFlowStatementService cashFlowStatementService; + + @GetMapping("/get") + @Operation(summary = "获得现金流量表") + @PreAuthorize("@ss.hasPermission('fms:report:cash-flow-statement:query')") + public CommonResult> getCashFlowStatement( + @Valid FmsReportListReqVO listReqVO) { + return success(cashFlowStatementService.getCashFlowStatement(listReqVO, getLoginUserId())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出现金流量表") + @PreAuthorize("@ss.hasPermission('fms:report:cash-flow-statement:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportCashFlowStatement(@Valid FmsReportListReqVO listReqVO, + HttpServletResponse response) throws IOException { + List list = cashFlowStatementService.getCashFlowStatement( + listReqVO, getLoginUserId()); + ExcelUtils.write(response, "现金流量表.xls", "现金流量表", FmsReportItemRespVO.class, list); + } + + @PutMapping("/update") + @Operation(summary = "更新现金流量表") + @PreAuthorize("@ss.hasPermission('fms:report:cash-flow-statement:update')") + public CommonResult updateCashFlowStatement( + @Valid @RequestBody FmsCashFlowStatementUpdateReqVO updateReqVO) { + cashFlowStatementService.updateCashFlowStatement(updateReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/check") + @Operation(summary = "检查现金流量表") + @PreAuthorize("@ss.hasPermission('fms:report:cash-flow-statement:query')") + public CommonResult checkCashFlowStatement( + @Valid FmsReportListReqVO listReqVO) { + return success(cashFlowStatementService.checkCashFlowStatement(listReqVO, getLoginUserId())); + } + + @GetMapping("/adjustment/list") + @Operation(summary = "获得现金流量辅助数据列表") + @PreAuthorize("@ss.hasPermission('fms:report:cash-flow-statement:query')") + public CommonResult> getCashFlowAdjustmentList(@Valid FmsReportListReqVO listReqVO) { + return success(cashFlowStatementService.getCashFlowAdjustmentList(listReqVO, getLoginUserId())); + } + + @PutMapping("/adjustment/update") + @Operation(summary = "更新现金流量辅助数据") + @PreAuthorize("@ss.hasPermission('fms:report:cash-flow-statement:update')") + public CommonResult updateCashFlowAdjustment(@Valid @RequestBody FmsCashFlowAdjustmentUpdateReqVO updateReqVO) { + cashFlowStatementService.updateCashFlowAdjustment(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/adjustment/update-formula") + @Operation(summary = "更新现金流量辅助数据公式") + @PreAuthorize("@ss.hasPermission('fms:report:cash-flow-statement:update')") + public CommonResult updateCashFlowAdjustmentFormula(@Valid @RequestBody FmsReportFormulaUpdateReqVO updateReqVO) { + cashFlowStatementService.updateCashFlowAdjustmentFormula(updateReqVO, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsIncomeStatementController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsIncomeStatementController.java new file mode 100644 index 0000000..43e0778 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/FmsIncomeStatementController.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.income.FmsIncomeStatementCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - FMS 利润表") +@RestController +@RequestMapping("/fms/report/income-statement") +@Validated +public class FmsIncomeStatementController { + + @Resource + private FmsIncomeStatementService incomeStatementService; + + @GetMapping("/get") + @Operation(summary = "获得利润表") + @PreAuthorize("@ss.hasPermission('fms:report:income-statement:query')") + public CommonResult> getIncomeStatement(@Valid FmsReportListReqVO listReqVO) { + return success(incomeStatementService.getIncomeStatement(listReqVO, getLoginUserId())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出利润表") + @PreAuthorize("@ss.hasPermission('fms:report:income-statement:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportIncomeStatement(@Valid FmsReportListReqVO listReqVO, HttpServletResponse response) throws IOException { + List list = incomeStatementService.getIncomeStatement(listReqVO, getLoginUserId()); + ExcelUtils.write(response, "利润表.xls", "利润表", FmsReportItemRespVO.class, list); + } + + @PutMapping("/update") + @Operation(summary = "更新利润表公式") + @PreAuthorize("@ss.hasPermission('fms:report:income-statement:update')") + public CommonResult updateIncomeStatementFormula(@Valid @RequestBody FmsReportFormulaUpdateReqVO updateReqVO) { + incomeStatementService.updateIncomeStatementFormula(updateReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/check") + @Operation(summary = "检查利润表") + @PreAuthorize("@ss.hasPermission('fms:report:income-statement:query')") + public CommonResult checkIncomeStatement(@Valid FmsReportListReqVO listReqVO) { + return success(incomeStatementService.checkIncomeStatement(listReqVO, getLoginUserId())); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportFormulaRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportFormulaRespVO.java new file mode 100644 index 0000000..368dae0 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportFormulaRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 报表公式项 Response VO") +@Data +public class FmsReportFormulaRespVO { + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "科目名称", example = "库存现金") + private String subjectName; + + @Schema(description = "科目编码", example = "1001") + private String subjectNumber; + + @Schema(description = "运算符", example = "+") + private String operator; + + @Schema(description = "取数规则", example = "0") + private Integer rules; + + @Schema(description = "期初金额", example = "100.00") + private BigDecimal openingAmount; + + @Schema(description = "期末金额", example = "200.00") + private BigDecimal closingAmount; + + @Schema(description = "本期金额", example = "100.00") + private BigDecimal currentAmount; + + @Schema(description = "本年累计金额", example = "200.00") + private BigDecimal yearAmount; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportFormulaUpdateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportFormulaUpdateReqVO.java new file mode 100644 index 0000000..5d24f2e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportFormulaUpdateReqVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - FMS 报表公式更新 Request VO") +@Data +public class FmsReportFormulaUpdateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "配置编号不能为空") + private Long id; + + @Schema(description = "公式项数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "公式项数组不能为空") + @Valid + private List formulas = new ArrayList<>(); + + @Schema(description = "管理后台 - FMS 报表公式项 Request VO") + @Data + public static class Formula { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "科目编号不能为空") + private Long subjectId; + + @Schema(description = "运算符", requiredMode = Schema.RequiredMode.REQUIRED, example = "+") + @NotNull(message = "运算符不能为空") + @Pattern(regexp = "[+-]", message = "运算符必须是 + 或 -") + private String operator; + + @Schema(description = "取数规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "取数规则不能为空") + @InEnum(value = FmsFormulaRuleEnum.class, message = "取数规则必须是 {value}") + private Integer rules; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportItemRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportItemRespVO.java new file mode 100644 index 0000000..2820415 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportItemRespVO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 财务报表项目 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsReportItemRespVO { + + @Schema(description = "配置编号", example = "1024") + private Long id; + + @Schema(description = "项目名称", example = "货币资金") + @ExcelProperty("项目") + private String name; + + @Schema(description = "行次", example = "1") + @ExcelProperty("行次") + private Integer rowNo; + + @Schema(description = "层级", example = "2") + private Integer level; + + @Schema(description = "是否可编辑", example = "true") + private Boolean editable; + + @Schema(description = "公式") + private String formula; + + @Schema(description = "期初金额", example = "100.00") + private BigDecimal openingAmount; + + @Schema(description = "期末金额", example = "200.00") + private BigDecimal closingAmount; + + @Schema(description = "本年累计金额", example = "200.00") + @ExcelProperty("本年累计金额") + private BigDecimal yearAmount; + + @Schema(description = "本期金额", example = "100.00") + @ExcelProperty("本期金额") + private BigDecimal currentAmount; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportListReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportListReqVO.java new file mode 100644 index 0000000..008a59c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportListReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 财务报表查询 Request VO") +@Data +public class FmsReportListReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "开始会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotNull(message = "开始会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "开始会计期间格式不正确") + private String startMonth; + + @Schema(description = "结束会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotNull(message = "结束会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "结束会计期间格式不正确") + private String endMonth; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/balance/FmsBalanceSheetCheckRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/balance/FmsBalanceSheetCheckRespVO.java new file mode 100644 index 0000000..31e2582 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/balance/FmsBalanceSheetCheckRespVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 资产负债表检查 Response VO") +@Data +public class FmsBalanceSheetCheckRespVO { + + @Schema(description = "报表是否平衡", example = "true") + private Boolean balanced; + + @Schema(description = "初始余额是否平衡", example = "true") + private Boolean initialBalanceBalanced; + + @Schema(description = "损益是否已结转", example = "true") + private Boolean profitLossTransferred; + + @Schema(description = "年初余额差额", example = "0.00") + private BigDecimal openingDifferenceAmount; + + @Schema(description = "期末余额差额", example = "0.00") + private BigDecimal closingDifferenceAmount; + + @Schema(description = "未纳入报表公式的科目") + private List unmappedSubjects; + + @Schema(description = "管理后台 - FMS 未纳入报表公式的科目") + @Data + public static class UnmappedSubject { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + private String code; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金") + private String name; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/balance/FmsBalanceSheetRowRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/balance/FmsBalanceSheetRowRespVO.java new file mode 100644 index 0000000..18fe2ca --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/balance/FmsBalanceSheetRowRespVO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 资产负债表行 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsBalanceSheetRowRespVO { + + @Schema(description = "行编号", example = "1") + private Integer rowId; + + @Schema(description = "资产项目配置编号", example = "1024") + private Long assetId; + + @Schema(description = "资产项目名称", example = "货币资金") + @ExcelProperty("资产") + private String assetName; + + @Schema(description = "资产项目行次", example = "1") + @ExcelProperty("行次") + private Integer assetRowNo; + + @Schema(description = "资产项目期末余额", example = "200.00") + @ExcelProperty("期末余额") + private BigDecimal assetClosingAmount; + + @Schema(description = "资产项目年初余额", example = "100.00") + @ExcelProperty("年初余额") + private BigDecimal assetOpeningAmount; + + @Schema(description = "资产项目层级", example = "2") + private Integer assetLevel; + + @Schema(description = "资产项目是否可编辑", example = "true") + private Boolean assetEditable; + + @Schema(description = "资产项目公式") + private String assetFormula; + + @Schema(description = "负债和所有者权益项目配置编号", example = "1024") + private Long liabilityId; + + @Schema(description = "负债和所有者权益项目名称", example = "短期借款") + @ExcelProperty("负债和所有者权益") + private String liabilityName; + + @Schema(description = "负债和所有者权益项目行次", example = "31") + @ExcelProperty("行次") + private Integer liabilityRowNo; + + @Schema(description = "负债和所有者权益项目期末余额", example = "200.00") + @ExcelProperty("期末余额") + private BigDecimal liabilityClosingAmount; + + @Schema(description = "负债和所有者权益项目年初余额", example = "100.00") + @ExcelProperty("年初余额") + private BigDecimal liabilityOpeningAmount; + + @Schema(description = "负债和所有者权益项目层级", example = "2") + private Integer liabilityLevel; + + @Schema(description = "负债和所有者权益项目是否可编辑", example = "true") + private Boolean liabilityEditable; + + @Schema(description = "负债和所有者权益项目公式") + private String liabilityFormula; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowAdjustmentRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowAdjustmentRespVO.java new file mode 100644 index 0000000..17e9503 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowAdjustmentRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 现金流量辅助数据 Response VO") +@Data +public class FmsCashFlowAdjustmentRespVO { + + @Schema(description = "数据编号", example = "1024") + private Long id; + + @Schema(description = "项目名称", example = "支付给职工的工资") + private String name; + + @Schema(description = "行次", example = "1") + private Integer rowNo; + + @Schema(description = "公式") + private String formula; + + @Schema(description = "说明") + private String remark; + + @Schema(description = "是否可编辑", example = "true") + private Boolean editable; + + @Schema(description = "本期金额", example = "100.00") + private BigDecimal currentAmount; + + @Schema(description = "本年累计金额", example = "200.00") + private BigDecimal yearAmount; + + @Schema(description = "层级", example = "1") + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowAdjustmentUpdateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowAdjustmentUpdateReqVO.java new file mode 100644 index 0000000..2462db8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowAdjustmentUpdateReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - FMS 现金流量辅助数据更新 Request VO") +@Data +public class FmsCashFlowAdjustmentUpdateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "辅助数据项", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "辅助数据项不能为空") + @Valid + private List items = new ArrayList<>(); + + @Schema(description = "管理后台 - FMS 现金流量辅助数据更新项 Request VO") + @Data + public static class Item { + + @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "数据编号不能为空") + private Long id; + + @Schema(description = "本期金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @NotNull(message = "本期金额不能为空") + private BigDecimal currentAmount; + + @Schema(description = "本年累计金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "200.00") + @NotNull(message = "本年累计金额不能为空") + private BigDecimal yearAmount; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowCheckRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowCheckRespVO.java new file mode 100644 index 0000000..09654d8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowCheckRespVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow; + +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - FMS 现金流量表检查 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsCashFlowCheckRespVO extends FmsBalanceSheetCheckRespVO { + + @Schema(description = "资产负债表是否满足现金流量表取数条件", example = "true") + private Boolean balanceSheetReady; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowStatementUpdateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowStatementUpdateReqVO.java new file mode 100644 index 0000000..7801cc4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/cashflow/FmsCashFlowStatementUpdateReqVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - FMS 现金流量表修改 Request VO") +@Data +public class FmsCashFlowStatementUpdateReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "开始会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotNull(message = "开始会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "开始会计期间格式不正确") + private String startMonth; + + @Schema(description = "结束会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotNull(message = "结束会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "结束会计期间格式不正确") + private String endMonth; + + @Schema(description = "现金流量表项目", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "现金流量表项目不能为空") + @Valid + private List items = new ArrayList<>(); + + @Schema(description = "管理后台 - FMS 现金流量表修改项 Request VO") + @Data + public static class Item { + + @Schema(description = "报表项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "报表项目编号不能为空") + private Long id; + + @Schema(description = "本期金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @NotNull(message = "本期金额不能为空") + private BigDecimal currentAmount; + + @Schema(description = "本年累计金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "200.00") + @NotNull(message = "本年累计金额不能为空") + private BigDecimal yearAmount; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/income/FmsIncomeStatementCheckRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/income/FmsIncomeStatementCheckRespVO.java new file mode 100644 index 0000000..5a67733 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/income/FmsIncomeStatementCheckRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.fms.controller.admin.report.vo.income; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 利润表检查 Response VO") +@Data +public class FmsIncomeStatementCheckRespVO { + + @Schema(description = "净利润与未分配利润变动是否一致", example = "true") + private Boolean balanced; + + @Schema(description = "利润表与资产负债表勾稽差额", example = "0.00") + private BigDecimal differenceAmount; + + @Schema(description = "未纳入报表公式的科目") + private List unmappedSubjects; + + @Schema(description = "管理后台 - FMS 未纳入报表公式的科目") + @Data + public static class UnmappedSubject { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + private String code; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金") + private String name; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/FmsVoucherController.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/FmsVoucherController.java new file mode 100644 index 0000000..1d84d8f --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/FmsVoucherController.java @@ -0,0 +1,380 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherAttachmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherExportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportTemplateVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherMoveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherReviewReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherTidyReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.enums.common.FmsBalanceDirectionEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; + +@Tag(name = "管理后台 - FMS 凭证") +@RestController +@RequestMapping("/fms/voucher") +@Validated +public class FmsVoucherController { + + @Resource + private FmsVoucherService voucherService; + @Resource + private FmsClosingVoucherService closingVoucherService; + @Resource + private FmsVoucherWordService voucherWordService; + @Resource + private FmsLedgerService ledgerService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private FileApi fileApi; + + @PostMapping("/create") + @Operation(summary = "创建凭证") + @PreAuthorize("@ss.hasPermission('fms:voucher:create')") + public CommonResult createVoucher(@Valid @RequestBody FmsVoucherSaveReqVO createReqVO) { + return success(voucherService.createVoucher(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新凭证") + @PreAuthorize("@ss.hasPermission('fms:voucher:update')") + public CommonResult updateVoucher(@Valid @RequestBody FmsVoucherSaveReqVO updateReqVO) { + voucherService.updateVoucher(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-attachments") + @Operation(summary = "更新凭证附件") + @PreAuthorize("@ss.hasPermission('fms:voucher:update')") + public CommonResult updateVoucherAttachments( + @Valid @RequestBody FmsVoucherAttachmentUpdateReqVO updateReqVO) { + voucherService.updateVoucherAttachments(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "删除凭证") + @PreAuthorize("@ss.hasPermission('fms:voucher:delete')") + public CommonResult deleteVoucherList( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("ids") @NotEmpty List ids) { + voucherService.deleteVoucherList(accountSetId, ids, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-review-status") + @Operation(summary = "审核或反审核凭证") + @PreAuthorize("@ss.hasPermission('fms:voucher:review')") + public CommonResult updateVoucherReviewStatus(@Valid @RequestBody FmsVoucherReviewReqVO reviewReqVO) { + voucherService.updateVoucherReviewStatus(reviewReqVO.getAccountSetId(), + reviewReqVO.getIds(), reviewReqVO.getStatus(), getLoginUserId()); + return success(true); + } + + @PutMapping("/tidy") + @Operation(summary = "整理凭证") + @PreAuthorize("@ss.hasPermission('fms:voucher:tidy')") + public CommonResult tidyVoucher(@Valid @RequestBody FmsVoucherTidyReqVO tidyReqVO) { + voucherService.tidyVoucher(tidyReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/move") + @Operation(summary = "移动凭证") + @PreAuthorize("@ss.hasPermission('fms:voucher:move')") + public CommonResult moveVoucher(@Valid @RequestBody FmsVoucherMoveReqVO moveReqVO) { + voucherService.moveVoucher(moveReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得凭证详情") + @PreAuthorize("@ss.hasAnyPermissions('fms:voucher:query', 'fms:voucher:update')") + public CommonResult getVoucher(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("id") @NotNull Long id) { + FmsVoucherDO voucher = voucherService.getVoucher(accountSetId, id, getLoginUserId()); + return success(buildVoucherRespVO(accountSetId, voucher)); + } + + @GetMapping("/subject-balance-list") + @Operation(summary = "获得凭证科目余额列表") + @PreAuthorize("@ss.hasAnyPermissions('fms:voucher:query', 'fms:voucher:create', 'fms:voucher:update')") + public CommonResult> getVoucherSubjectBalanceList( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("month") @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确") String month) { + List balances = ledgerService.getSubjectBalanceList( + new FmsLedgerListReqVO().setAccountSetId(accountSetId).setStartMonth(month).setEndMonth(month), getLoginUserId()); + // 拼接 VO 返回 + List respVOList = new ArrayList<>(); + appendVoucherSubjectBalanceRespVOList(respVOList, balances); + return success(respVOList); + } + + @GetMapping("/auxiliary-balance") + @Operation(summary = "获得凭证辅助核算组合余额") + @PreAuthorize("@ss.hasAnyPermissions('fms:voucher:query', 'fms:voucher:create', 'fms:voucher:update')") + public CommonResult getVoucherAuxiliaryBalance( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("month") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确") String month, + @RequestParam("subjectId") @NotNull Long subjectId, + @RequestParam("auxiliaryItemIds") @NotEmpty List auxiliaryItemIds) { + BigDecimal signedBalance = ledgerService.getAuxiliaryCombinationBalance( + accountSetId, month, subjectId, auxiliaryItemIds, getLoginUserId()); + return success(new FmsVoucherSubjectBalanceRespVO().setSubjectId(subjectId) + .setBalanceDirection(FmsBalanceDirectionEnum.valueOf(signedBalance).getName()) + .setBalance(signedBalance.abs())); + } + + @GetMapping("/page") + @Operation(summary = "获得凭证分页") + @PreAuthorize("@ss.hasPermission('fms:voucher:query')") + public CommonResult> getVoucherPage(@Valid FmsVoucherPageReqVO pageReqVO) { + PageResult pageResult = voucherService.getVoucherPage(pageReqVO, getLoginUserId()); + return success(new PageResult<>( + buildVoucherRespVOList(pageReqVO.getAccountSetId(), pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/print-list") + @Operation(summary = "获得待打印凭证列表") + @PreAuthorize("@ss.hasPermission('fms:voucher:print')") + public CommonResult> getVoucherPrintList(@Valid FmsVoucherPageReqVO queryReqVO) { + queryReqVO.setPageSize(PAGE_SIZE_NONE); + List vouchers = voucherService.getVoucherPage(queryReqVO, getLoginUserId()).getList(); + return success(buildVoucherRespVOList(queryReqVO.getAccountSetId(), vouchers)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出凭证") + @PreAuthorize("@ss.hasPermission('fms:voucher:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportVoucherExcel(@Valid FmsVoucherPageReqVO exportReqVO, HttpServletResponse response) + throws IOException { + exportReqVO.setPageSize(PAGE_SIZE_NONE); + List list = voucherService.getVoucherPage(exportReqVO, getLoginUserId()).getList(); + List vouchers = buildVoucherRespVOList(exportReqVO.getAccountSetId(), list); + ExcelUtils.write(response, "凭证列表.xls", "凭证列表", FmsVoucherExportRespVO.class, + buildVoucherExportRespVOList(vouchers)); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得凭证导入模板") + @PreAuthorize("@ss.hasPermission('fms:voucher:import')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getVoucherImportTemplate(@RequestParam("accountSetId") @NotNull Long accountSetId, + HttpServletResponse response) throws IOException { + FmsVoucherImportTemplateVO templateData = voucherService.getVoucherImportTemplateData(accountSetId, getLoginUserId()); + ServletUtils.writeAttachment(response, "凭证导入模板.xlsx", FmsVoucherImportExcelHelper.writeTemplate(templateData)); + } + + @PostMapping("/import") + @Operation(summary = "导入凭证") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true), + @Parameter(name = "file", description = "Excel 文件", required = true) + }) + @PreAuthorize("@ss.hasPermission('fms:voucher:import')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult importVoucher(@RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("file") MultipartFile file) throws IOException { + FmsVoucherImportTemplateVO templateData = voucherService.getVoucherImportTemplateData(accountSetId, getLoginUserId()); + // 1. 解析导入文件 + List rows; + try { + rows = FmsVoucherImportExcelHelper.read(file, templateData.getAuxiliaryTypes()); + } catch (IllegalArgumentException | IOException exception) { + throw exception(VOUCHER_IMPORT_TEMPLATE_INVALID); + } + // 2. 导入凭证 + FmsVoucherImportRespVO respVO = voucherService.importVoucher( + accountSetId, rows, getLoginUserId()); + + // 3. 生成错误数据文件 + if (CollUtil.isNotEmpty(respVO.getErrorRows())) { + byte[] errorFile = FmsVoucherImportExcelHelper.writeErrorFile(respVO.getErrorRows()); + respVO.setErrorFileUrl(fileApi.createFile(errorFile, + "凭证导入错误数据.xlsx", "fms/voucher/import", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")); + } + return success(respVO); + } + + @GetMapping("/statistics/list") + @Operation(summary = "获得凭证汇总") + @PreAuthorize("@ss.hasPermission('fms:voucher:statistics:query')") + public CommonResult> getVoucherStatisticsList(@Valid FmsVoucherStatisticsReqVO queryReqVO) { + return success(voucherService.getVoucherStatisticsList(queryReqVO, getLoginUserId())); + } + + @GetMapping("/statistics/export-excel") + @Operation(summary = "导出凭证汇总") + @PreAuthorize("@ss.hasPermission('fms:voucher:statistics:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportVoucherStatisticsExcel( + @Valid FmsVoucherStatisticsReqVO queryReqVO, HttpServletResponse response) throws IOException { + List list = voucherService.getVoucherStatisticsList(queryReqVO, getLoginUserId()); + ExcelUtils.write(response, "凭证汇总表.xls", "凭证汇总", FmsVoucherStatisticsRespVO.class, list); + } + + @GetMapping("/next-number") + @Operation(summary = "获得下一凭证号") + @Parameters({ + @Parameter(name = "accountSetId", description = "账套编号", required = true), + @Parameter(name = "voucherWordId", description = "凭证字编号", required = true), + @Parameter(name = "voucherTime", description = "凭证日期", required = true) + }) + @PreAuthorize("@ss.hasAnyPermissions('fms:voucher:query', 'fms:voucher:create', 'fms:voucher:update')") + public CommonResult getNextVoucherNumber( + @RequestParam("accountSetId") @NotNull Long accountSetId, + @RequestParam("voucherWordId") @NotNull Long voucherWordId, + @RequestParam("voucherTime") @NotNull + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime voucherTime) { + return success(voucherService.getNextVoucherNumber(accountSetId, voucherWordId, voucherTime, getLoginUserId())); + } + + // ==================== 拼接 VO ==================== + + private FmsVoucherRespVO buildVoucherRespVO(Long accountSetId, FmsVoucherDO voucher) { + return CollUtil.getFirst(buildVoucherRespVOList( + accountSetId, Collections.singletonList(voucher))); + } + + private void appendVoucherSubjectBalanceRespVOList( + List respVOList, + List balances) { + balances.forEach(balance -> { + BigDecimal debitAmount = balance.getEndingDebitAmount() != null + ? balance.getEndingDebitAmount() : BigDecimal.ZERO; + BigDecimal creditAmount = balance.getEndingCreditAmount() != null + ? balance.getEndingCreditAmount() : BigDecimal.ZERO; + respVOList.add(new FmsVoucherSubjectBalanceRespVO().setSubjectId(balance.getSubjectId()) + .setBalanceDirection(balance.getEndingBalanceDirection()) + .setBalance(debitAmount.add(creditAmount))); + appendVoucherSubjectBalanceRespVOList(respVOList, balance.getChildren()); + }); + } + + private List buildVoucherRespVOList( + Long accountSetId, List vouchers) { + if (CollUtil.isEmpty(vouchers)) { + return Collections.emptyList(); + } + // 1. 批量查询凭证字、分录和用户 + Set voucherIds = convertSet(vouchers, FmsVoucherDO::getId); + Map voucherWordMap = voucherWordService.getVoucherWordMap(accountSetId); + Map> entryMap = voucherService.getVoucherEntryMap(voucherIds); + Map userMap = adminUserApi.getUserMap(convertListByFlatMap( + vouchers, voucher -> Stream.of(NumberUtils.parseLong(voucher.getCreator()), + voucher.getReviewerUserId()))); + Set closingVoucherIds = closingVoucherService.getClosingVoucherIdSet(accountSetId, voucherIds); + + // 2. 拼接凭证响应 + return convertList(vouchers, voucher -> { + FmsVoucherRespVO respVO = BeanUtils.toBean(voucher, FmsVoucherRespVO.class); + respVO.setEntries(BeanUtils.toBean( + entryMap.getOrDefault(voucher.getId(), Collections.emptyList()), + FmsVoucherRespVO.Entry.class)); + respVO.setCreatorUserId(NumberUtils.parseLong(voucher.getCreator())); + respVO.setClosingGenerated(closingVoucherIds.contains(voucher.getId())); + MapUtils.findAndThen(voucherWordMap, voucher.getVoucherWordId(), + word -> respVO.setVoucherWordName(word.getName())); + MapUtils.findAndThen(userMap, respVO.getCreatorUserId(), + user -> respVO.setCreatorUserName(user.getNickname())); + MapUtils.findAndThen(userMap, respVO.getReviewerUserId(), + user -> respVO.setReviewerUserName(user.getNickname())); + return respVO; + }); + } + + private List buildVoucherExportRespVOList( + List vouchers) { + return convertListByFlatMap(vouchers, voucher -> voucher.getEntries().stream().map(entry -> { + String subjectName = entry.getSubjectName(); + if (CollUtil.isNotEmpty(entry.getAuxiliaries())) { + subjectName += " / " + String.join("、", convertList( + entry.getAuxiliaries(), FmsVoucherRespVO.Entry.AuxiliaryItem::getName)); + } + return new FmsVoucherExportRespVO().setVoucherTime(voucher.getVoucherTime()) + .setVoucherWordName(voucher.getVoucherWordName()) + .setVoucherNumber(voucher.getVoucherNumber()).setDigest(entry.getDigest()) + .setSubjectCode(entry.getSubjectCode()).setSubjectName(subjectName) + .setDebitAmount(entry.getDebitAmount()).setCreditAmount(entry.getCreditAmount()) + .setAttachmentCount(voucher.getAttachmentCount()) + .setCreatorUserName(voucher.getCreatorUserName()) + .setReviewerUserName(voucher.getReviewerUserName()); + })); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/FmsVoucherImportExcelHelper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/FmsVoucherImportExcelHelper.java new file mode 100644 index 0000000..7dfe7b5 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/FmsVoucherImportExcelHelper.java @@ -0,0 +1,328 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportTemplateVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import lombok.experimental.UtilityClass; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.addComment; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.createHeaderStyle; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.getCellText; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.getDecimal; +import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.isEmptyRow; + +/** + * FMS 凭证导入 Excel 辅助类 + * + * @author 芋道源码 + */ +@UtilityClass +public final class FmsVoucherImportExcelHelper { + + private static final String DATA_SHEET_NAME = "凭证模板"; + private static final String SUBJECT_SHEET_NAME = "科目数据"; + private static final String AUXILIARY_SHEET_NAME = "辅助核算项目数据"; + private static final List BASE_HEADERS = Arrays.asList( + "日期", "凭证字", "凭证号", "附件数", "摘要", "科目编码", "科目名称", "借方金额", "贷方金额"); + + private static final List REQUIRED_HEADERS = Arrays.asList( + "日期", "凭证字", "凭证号", "摘要", "科目编码", "借方金额", "贷方金额"); + + /** + * 读取凭证导入文件 + * + * 按模板表头校验文件合法性,并根据动态辅助核算类别列解析数据 + * + * @param file 导入文件 + * @param auxiliaryTypes 辅助核算类别数组 + * @return 凭证导入数据数组 + */ + public static List read(MultipartFile file, + List auxiliaryTypes) throws IOException { + try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) { + // 1. 校验模板 sheet 和表头 + Sheet sheet = workbook.getSheet(DATA_SHEET_NAME); + if (sheet == null || sheet.getPhysicalNumberOfRows() == 0) { + throw new IllegalArgumentException("凭证模板工作表不存在"); + } + Row headerRow = sheet.getRow(sheet.getFirstRowNum()); + Map headerIndexes = getHeaderIndexes(headerRow); + if (!headerIndexes.keySet().containsAll(REQUIRED_HEADERS)) { + throw new IllegalArgumentException("凭证模板表头不完整"); + } + // 2. 构建动态辅助核算类别映射 + Map auxiliaryTypeMap = new LinkedHashMap<>(); + auxiliaryTypes.forEach(type -> auxiliaryTypeMap.put(type.getName(), type.getId())); + // 3. 逐行解析,跳过空行 + List rows = new ArrayList<>(); + for (int rowIndex = headerRow.getRowNum() + 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (isEmptyRow(row, null, headerIndexes.values())) { + continue; + } + rows.add(parseRow(row, headerIndexes, auxiliaryTypeMap)); + } + return rows; + } + } + + /** + * 生成凭证导入模板 + * + * 模板根据账套辅助核算类别动态增加列,并提供凭证字下拉选项和基础数据工作表 + * + * @param templateData 模板基础数据 + * @return Excel 文件字节数组 + */ + public static byte[] writeTemplate(FmsVoucherImportTemplateVO templateData) throws IOException { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + // 1. 构建动态表头和示例数据 + List headers = new ArrayList<>(BASE_HEADERS); + templateData.getAuxiliaryTypes().forEach(type -> headers.add(type.getName())); + headers.add("数量"); + headers.add("单价"); + Sheet dataSheet = workbook.createSheet(DATA_SHEET_NAME); + writeHeader(workbook, dataSheet, headers); + writeExampleRow(dataSheet, templateData); + // 2. 增加凭证字下拉选项和填写说明 + addVoucherWordValidation(dataSheet, templateData.getVoucherWords(), headers.indexOf("凭证字")); + addComment(dataSheet.getRow(0).getCell(headers.indexOf("数量")), + "选填,科目启用数量核算时可填写"); + addComment(dataSheet.getRow(0).getCell(headers.indexOf("单价")), + "选填,与数量同时填写时,数量乘单价应等于分录金额"); + // 3. 写入科目和辅助核算基础数据 + writeSubjectSheet(workbook, templateData.getSubjects(), templateData.getAuxiliaryTypes()); + writeAuxiliarySheet(workbook, templateData.getAuxiliaryTypes(), templateData.getAuxiliaryItems()); + workbook.write(output); + return output.toByteArray(); + } + } + + /** + * 生成凭证导入错误文件 + * + * @param errorRows 导入错误数据数组 + * @return Excel 文件字节数组 + */ + public static byte[] writeErrorFile(List errorRows) throws IOException { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + // 1. 收集原始文件表头 + Sheet sheet = workbook.createSheet("错误数据"); + Set sourceHeaders = new LinkedHashSet<>(); + errorRows.forEach(row -> sourceHeaders.addAll(row.getSourceValues().keySet())); + List headers = new ArrayList<>(); + headers.add("错误信息"); + headers.addAll(sourceHeaders); + writeHeader(workbook, sheet, headers); + // 2. 写入错误信息和原始数据 + for (int index = 0; index < errorRows.size(); index++) { + FmsVoucherImportExcelVO errorRow = errorRows.get(index); + Row row = sheet.createRow(index + 1); + row.createCell(0).setCellValue(String.join(";", errorRow.getErrors())); + int columnIndex = 1; + for (String header : sourceHeaders) { + row.createCell(columnIndex++).setCellValue( + StrUtil.nullToEmpty(errorRow.getSourceValues().get(header))); + } + } + setColumnWidths(sheet, headers.size()); + workbook.write(output); + return output.toByteArray(); + } + } + + private static FmsVoucherImportExcelVO parseRow(Row row, Map headerIndexes, + Map auxiliaryTypeMap) { + FmsVoucherImportExcelVO result = new FmsVoucherImportExcelVO().setRowNumber(row.getRowNum() + 1); + headerIndexes.forEach((header, index) -> result.getSourceValues().put(header, getCellText(row, index, null))); + result.setVoucherTime(getDateTime(row, headerIndexes.get("日期"))); + result.setVoucherWordName(getCellText(row, headerIndexes.get("凭证字"), null)); + result.setVoucherNumber(getInteger(row, headerIndexes.get("凭证号"))); + result.setAttachmentCount(getInteger(row, headerIndexes.get("附件数"))); + result.setDigest(getCellText(row, headerIndexes.get("摘要"), null)); + result.setSubjectCode(getCellText(row, headerIndexes.get("科目编码"), null)); + result.setSubjectName(getCellText(row, headerIndexes.get("科目名称"), null)); + result.setDebitAmount(getDecimalOrNull(row, headerIndexes.get("借方金额"))); + result.setCreditAmount(getDecimalOrNull(row, headerIndexes.get("贷方金额"))); + result.setQuantity(getDecimalOrNull(row, headerIndexes.get("数量"))); + result.setUnitPrice(getDecimalOrNull(row, headerIndexes.get("单价"))); + auxiliaryTypeMap.forEach((name, id) -> { + if (headerIndexes.containsKey(name)) { + result.getAuxiliaryCodes().put(id, getCellText(row, headerIndexes.get(name), null)); + } + }); + return result; + } + + private static Map getHeaderIndexes(Row headerRow) { + if (headerRow == null) { + return Collections.emptyMap(); + } + Map indexes = new LinkedHashMap<>(); + for (Cell cell : headerRow) { + String header = getCellText(headerRow, cell.getColumnIndex(), null); + if (StrUtil.isNotBlank(header)) { + indexes.put(header, cell.getColumnIndex()); + } + } + return indexes; + } + + private static Integer getInteger(Row row, Integer columnIndex) { + BigDecimal value = getDecimalOrNull(row, columnIndex); + if (value == null) { + return null; + } + try { + return value.intValueExact(); + } catch (ArithmeticException ignored) { + return null; + } + } + + private static BigDecimal getDecimalOrNull(Row row, Integer columnIndex) { + try { + return getDecimal(row, columnIndex, null); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static LocalDateTime getDateTime(Row row, Integer columnIndex) { + Cell cell = row.getCell(columnIndex); + if (cell != null && cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { + return cell.getLocalDateTimeCellValue().toLocalDate().atStartOfDay(); + } + String value = getCellText(row, columnIndex, null); + if (StrUtil.isBlank(value)) { + return null; + } + try { + return LocalDateTimeUtils.parse(value).toLocalDate().atStartOfDay(); + } catch (DateTimeException ignored) { + return null; + } + } + + private static void writeHeader(Workbook workbook, Sheet sheet, List headers) { + CellStyle style = createHeaderStyle(workbook, IndexedColors.LIGHT_BLUE); + Row row = sheet.createRow(0); + for (int index = 0; index < headers.size(); index++) { + Cell cell = row.createCell(index); + cell.setCellValue(headers.get(index)); + cell.setCellStyle(style); + } + sheet.createFreezePane(0, 1); + setColumnWidths(sheet, headers.size()); + } + + private static void writeExampleRow(Sheet sheet, FmsVoucherImportTemplateVO templateData) { + Row row = sheet.createRow(1); + row.createCell(0).setCellValue(LocalDate.now().toString()); + if (CollUtil.isNotEmpty(templateData.getVoucherWords())) { + row.createCell(1).setCellValue(templateData.getVoucherWords().get(0).getName()); + } + row.createCell(2).setCellValue(1); + row.createCell(3).setCellValue(0); + row.createCell(4).setCellValue("示例摘要,导入前请删除示例行"); + } + + private static void writeSubjectSheet(Workbook workbook, List subjects, + List auxiliaryTypes) { + Sheet sheet = workbook.createSheet(SUBJECT_SHEET_NAME); + List headers = Arrays.asList("科目编码", "科目名称", "数量核算", "计量单位", "辅助核算"); + writeHeader(workbook, sheet, headers); + Map auxiliaryTypeNameMap = new LinkedHashMap<>(); + auxiliaryTypes.forEach(type -> auxiliaryTypeNameMap.put(type.getId(), type.getName())); + for (int index = 0; index < subjects.size(); index++) { + FmsSubjectDO subject = subjects.get(index); + Row row = sheet.createRow(index + 1); + row.createCell(0).setCellValue(subject.getCode()); + row.createCell(1).setCellValue(subject.getName()); + row.createCell(2).setCellValue(Boolean.TRUE.equals(subject.getQuantityAccounting()) ? "是" : "否"); + row.createCell(3).setCellValue(StrUtil.nullToEmpty(subject.getQuantityUnit())); + List auxiliaryNames = new ArrayList<>(); + if (subject.getAuxiliaryTypeIds() != null) { + subject.getAuxiliaryTypeIds().forEach( + auxiliaryTypeId -> auxiliaryNames.add(auxiliaryTypeNameMap.get(auxiliaryTypeId))); + } + row.createCell(4).setCellValue(String.join("、", auxiliaryNames)); + } + setColumnWidths(sheet, headers.size()); + } + + private static void writeAuxiliarySheet(Workbook workbook, List auxiliaryTypes, + List auxiliaryItems) { + Sheet sheet = workbook.createSheet(AUXILIARY_SHEET_NAME); + List headers = Arrays.asList("辅助核算类别", "编码", "名称"); + writeHeader(workbook, sheet, headers); + Map typeNameMap = new LinkedHashMap<>(); + auxiliaryTypes.forEach(type -> typeNameMap.put(type.getId(), type.getName())); + for (int index = 0; index < auxiliaryItems.size(); index++) { + FmsAuxiliaryItemDO item = auxiliaryItems.get(index); + Row row = sheet.createRow(index + 1); + row.createCell(0).setCellValue(StrUtil.nullToEmpty(typeNameMap.get(item.getAuxiliaryTypeId()))); + row.createCell(1).setCellValue(item.getCode()); + row.createCell(2).setCellValue(item.getName()); + } + setColumnWidths(sheet, headers.size()); + } + + private static void addVoucherWordValidation(Sheet sheet, List voucherWords, + int columnIndex) { + if (CollUtil.isEmpty(voucherWords)) { + return; + } + String[] values = voucherWords.stream().map(FmsVoucherWordDO::getName).toArray(String[]::new); + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = helper.createExplicitListConstraint(values); + DataValidation validation = helper.createValidation( + constraint, new CellRangeAddressList(1, + sheet.getWorkbook().getSpreadsheetVersion().getLastRowIndex(), columnIndex, columnIndex)); + validation.setShowErrorBox(true); + sheet.addValidationData(validation); + } + + private static void setColumnWidths(Sheet sheet, int columnCount) { + for (int index = 0; index < columnCount; index++) { + sheet.setColumnWidth(index, index == 4 ? 8_000 : 4_500); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherAttachmentUpdateReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherAttachmentUpdateReqVO.java new file mode 100644 index 0000000..994b7ab --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherAttachmentUpdateReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证附件更新 Request VO") +@Data +public class FmsVoucherAttachmentUpdateReqVO { + + @Schema(description = "凭证编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "凭证不能为空") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "附件地址数组") + @Size(max = 100, message = "凭证附件不能超过 100 个") + private List attachmentUrls; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherEntrySaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherEntrySaveReqVO.java new file mode 100644 index 0000000..0b11377 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherEntrySaveReqVO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证分录保存 Request VO") +@Data +public class FmsVoucherEntrySaveReqVO { + + @Schema(description = "凭证分录编号", example = "1024") + private Long id; + + @Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "支付办公用品款") + @NotBlank(message = "摘要不能为空") + @Size(max = 500, message = "摘要长度不能超过 500 个字符") + private String digest; + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "科目不能为空") + private Long subjectId; + + @Schema(description = "数量", example = "10") + @DecimalMin(value = "0", message = "数量不能小于 0") + @Digits(integer = 14, fraction = 4, message = "数量最多保留 4 位小数") + private BigDecimal quantity; + + @Schema(description = "单价", example = "20.50") + @DecimalMin(value = "0", message = "单价不能小于 0") + @Digits(integer = 12, fraction = 6, message = "单价最多保留 6 位小数") + private BigDecimal unitPrice; + + @Schema(description = "借方金额", example = "205.00") + @Digits(integer = 16, fraction = 2, message = "借方金额最多保留 2 位小数") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额", example = "205.00") + @Digits(integer = 16, fraction = 2, message = "贷方金额最多保留 2 位小数") + private BigDecimal creditAmount; + + @Schema(description = "辅助核算项目数组") + @Valid + private List auxiliaries; + + @Schema(description = "管理后台 - FMS 凭证分录辅助核算项目") + @Data + public static class AuxiliaryItem { + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + @NotNull(message = "辅助核算类别不能为空") + private Long typeId; + + @Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "辅助核算项目不能为空") + private Long itemId; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherExportRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherExportRespVO.java new file mode 100644 index 0000000..db12e3b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherExportRespVO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.DateTimeFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - FMS 凭证导出 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsVoucherExportRespVO { + + @Schema(description = "凭证日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("日期") + @DateTimeFormat("yyyy-MM-dd") + private LocalDateTime voucherTime; + + @Schema(description = "凭证字", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("凭证字") + private String voucherWordName; + + @Schema(description = "凭证号", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("凭证号") + private Integer voucherNumber; + + @Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("摘要") + private String digest; + + @Schema(description = "科目代码", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("科目代码") + private String subjectCode; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("科目名称") + private String subjectName; + + @Schema(description = "借方金额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("借方金额") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("贷方金额") + private BigDecimal creditAmount; + + @Schema(description = "附件数", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("附件数") + private Integer attachmentCount; + + @Schema(description = "制单人") + @ExcelProperty("制单人") + private String creatorUserName; + + @Schema(description = "审核人") + @ExcelProperty("审核人") + private String reviewerUserName; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportExcelVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportExcelVO.java new file mode 100644 index 0000000..6ac2c1c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportExcelVO.java @@ -0,0 +1,116 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * FMS 凭证导入 Excel VO + * + * @author 芋道源码 + */ +@Data +public class FmsVoucherImportExcelVO { + + /** + * Excel 行号 + */ + private Integer rowNumber; + + /** + * 凭证日期 + */ + @NotNull(message = "日期不能为空或格式不正确") + private LocalDateTime voucherTime; + + /** + * 凭证字 + */ + @NotBlank(message = "凭证字不能为空") + private String voucherWordName; + + /** + * 凭证号 + */ + @NotNull(message = "凭证号不能为空") + @Min(value = 1, message = "凭证号必须为正整数") + private Integer voucherNumber; + + /** + * 附件数 + */ + @Min(value = 0, message = "附件数不能小于 0") + private Integer attachmentCount; + + /** + * 摘要内容 + */ + private String digest; + + /** + * 科目编码 + */ + @NotBlank(message = "科目编码不能为空") + private String subjectCode; + + /** + * 科目名称 + */ + private String subjectName; + + /** + * 借方金额 + */ + @Digits(integer = 16, fraction = 2, message = "借方金额最多保留 2 位小数") + private BigDecimal debitAmount; + + /** + * 贷方金额 + */ + @Digits(integer = 16, fraction = 2, message = "贷方金额最多保留 2 位小数") + private BigDecimal creditAmount; + + /** + * 数量 + */ + @DecimalMin(value = "0", message = "数量不能小于 0") + @Digits(integer = 14, fraction = 4, message = "数量最多保留 4 位小数") + private BigDecimal quantity; + + /** + * 单价 + */ + @DecimalMin(value = "0", message = "单价不能小于 0") + @Digits(integer = 12, fraction = 6, message = "单价最多保留 6 位小数") + private BigDecimal unitPrice; + + /** + * 辅助核算类别编号与项目编码的映射 + */ + private Map auxiliaryCodes = new LinkedHashMap<>(); + /** + * Excel 表头与原始单元格值的映射 + */ + private Map sourceValues = new LinkedHashMap<>(); + /** + * 校验错误数组 + */ + private List errors = new ArrayList<>(); + + public void addError(String error) { + if (!errors.contains(error)) { + errors.add(error); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportRespVO.java new file mode 100644 index 0000000..40013f9 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证导入结果 Response VO") +@Data +public class FmsVoucherImportRespVO { + + @Schema(description = "总分录数", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer totalRowCount; + + @Schema(description = "成功分录数", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer successRowCount; + + @Schema(description = "失败分录数", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer failureRowCount; + + @Schema(description = "总凭证数", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer totalVoucherCount; + + @Schema(description = "成功凭证数", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer successVoucherCount; + + @Schema(description = "失败凭证数", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer failureVoucherCount; + + @Schema(description = "错误数据文件地址") + private String errorFileUrl; + + /** + * 错误数据数组 + * + * 仅用于生成错误数据文件,不直接返回前端 + */ + @JsonIgnore + private List errorRows; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportTemplateVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportTemplateVO.java new file mode 100644 index 0000000..666de41 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherImportTemplateVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +/** + * FMS 凭证导入模板 VO + * + * @author 芋道源码 + */ +@Data +@AllArgsConstructor +public class FmsVoucherImportTemplateVO { + + /** + * 凭证字数组 + */ + private List voucherWords; + /** + * 科目数组 + */ + private List subjects; + /** + * 辅助核算类别数组 + */ + private List auxiliaryTypes; + /** + * 辅助核算项目数组 + */ + private List auxiliaryItems; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherMoveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherMoveReqVO.java new file mode 100644 index 0000000..40dbb38 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherMoveReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 凭证移动 Request VO") +@Data +public class FmsVoucherMoveReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "凭证月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotBlank(message = "凭证月份不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "凭证月份格式必须为 yyyy-MM") + private String month; + + @Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + @NotNull(message = "凭证字不能为空") + private Long voucherWordId; + + @Schema(description = "原凭证号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6") + @NotNull(message = "原凭证号不能为空") + @Min(value = 1, message = "原凭证号必须大于 0") + private Integer sourceNumber; + + @Schema(description = "移动到的凭证号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "移动到的凭证号不能为空") + @Min(value = 1, message = "移动到的凭证号必须大于 0") + private Integer targetNumber; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherPageReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherPageReqVO.java new file mode 100644 index 0000000..86d8a81 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherPageReqVO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - FMS 凭证分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsVoucherPageReqVO extends PageParam { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "凭证编号数组") + @Size(max = 200, message = "凭证编号不能超过 200 个") + private List ids; + + @Schema(description = "凭证日期范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] voucherTime; + + @Schema(description = "凭证字编号", example = "11") + private Long voucherWordId; + + @Schema(description = "凭证号", example = "1") + private Integer voucherNumber; + + @Schema(description = "摘要关键词", example = "办公用品") + private String digest; + + @Schema(description = "科目编号", example = "1024") + private Long subjectId; + + @Schema(description = "最小金额", example = "100") + private BigDecimal minAmount; + + @Schema(description = "最大金额", example = "500") + private BigDecimal maxAmount; + + @Schema(description = "制单人后台用户编号", example = "1") + private Long creatorUserId; + + @Schema(description = "审核状态", example = "0") + @InEnum(FmsVoucherStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherRespVO.java new file mode 100644 index 0000000..15aa441 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherRespVO.java @@ -0,0 +1,129 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证 Response VO") +@Data +public class FmsVoucherRespVO { + + @Schema(description = "凭证编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long accountSetId; + + @Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long voucherWordId; + + @Schema(description = "凭证字", requiredMode = Schema.RequiredMode.REQUIRED) + private String voucherWordName; + + @Schema(description = "凭证号", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer voucherNumber; + + @Schema(description = "凭证日期", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime voucherTime; + + @Schema(description = "附件地址数组") + private List attachmentUrls; + + @Schema(description = "附件数", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer attachmentCount; + + @Schema(description = "借方金额", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal debitAmount; + + @Schema(description = "贷方金额", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal creditAmount; + + @Schema(description = "合计金额", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal total; + + @Schema(description = "审核状态", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer status; + + @Schema(description = "是否为结账生成凭证", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean closingGenerated; + + @Schema(description = "制单人后台用户编号") + private Long creatorUserId; + + @Schema(description = "制单人名称") + private String creatorUserName; + + @Schema(description = "审核人后台用户编号") + private Long reviewerUserId; + + @Schema(description = "审核人名称") + private String reviewerUserName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "凭证分录数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List entries; + + @Schema(description = "管理后台 - FMS 凭证分录 Response VO") + @Data + public static class Entry { + + @Schema(description = "分录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String digest; + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String subjectCode; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String subjectName; + + @Schema(description = "数量") + private BigDecimal quantity; + + @Schema(description = "单价") + private BigDecimal unitPrice; + + @Schema(description = "借方金额", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal debitAmount; + + @Schema(description = "贷方金额", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal creditAmount; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer sort; + + @Schema(description = "辅助核算组合编号") + private Long assistCombinationId; + + @Schema(description = "辅助核算项目数组") + private List auxiliaries; + + @Schema(description = "管理后台 - FMS 凭证分录辅助核算项目") + @Data + public static class AuxiliaryItem { + + @Schema(description = "辅助核算类型", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer type; + + @Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long typeId; + + @Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long itemId; + + @Schema(description = "辅助核算项目名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherReviewReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherReviewReqVO.java new file mode 100644 index 0000000..1db5dd3 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherReviewReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证审核 Request VO") +@Data +public class FmsVoucherReviewReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "凭证编号数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "凭证编号不能为空") + private List ids; + + @Schema(description = "审核状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "审核状态不能为空") + @InEnum(FmsVoucherStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSaveReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSaveReqVO.java new file mode 100644 index 0000000..d9ab2a2 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSaveReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.Min; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - FMS 凭证保存 Request VO") +@Data +public class FmsVoucherSaveReqVO { + + @Schema(description = "凭证编号", example = "1024") + private Long id; + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + @NotNull(message = "凭证字不能为空") + private Long voucherWordId; + + @Schema(description = "凭证号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "凭证号不能为空") + @Min(value = 1, message = "凭证号必须大于 0") + private Integer voucherNumber; + + @Schema(description = "凭证日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "凭证日期不能为空") + private LocalDateTime voucherTime; + + @Schema(description = "附单据张数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "附单据张数不能为空") + @Min(value = 0, message = "附单据张数不能小于 0") + private Integer attachmentCount; + + @Schema(description = "凭证分录数组", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @Size(min = 2, message = "凭证至少需要两条分录") + private List entries; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherStatisticsReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherStatisticsReqVO.java new file mode 100644 index 0000000..13beb85 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherStatisticsReqVO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 凭证汇总查询 Request VO") +@Data +public class FmsVoucherStatisticsReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套编号不能为空") + private Long accountSetId; + + @Schema(description = "开始会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotNull(message = "开始会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "开始会计期间格式不正确") + private String startMonth; + + @Schema(description = "结束会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-12") + @NotNull(message = "结束会计期间不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "结束会计期间格式不正确") + private String endMonth; + + @Schema(description = "凭证字编号", example = "1024") + private Long voucherWordId; + + @Schema(description = "最小凭证号", example = "1") + @Min(value = 1, message = "最小凭证号不能小于 1") + private Integer minVoucherNumber; + + @Schema(description = "最大凭证号", example = "100") + @Min(value = 1, message = "最大凭证号不能小于 1") + private Integer maxVoucherNumber; + + @Schema(description = "最小科目级次", example = "1") + @Min(value = 1, message = "最小科目级次不能小于 1") + @Max(value = 10, message = "最小科目级次不能大于 10") + private Integer minLevel; + + @Schema(description = "最大科目级次", example = "1") + @Min(value = 1, message = "最大科目级次不能小于 1") + @Max(value = 10, message = "最大科目级次不能大于 10") + private Integer maxLevel; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherStatisticsRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherStatisticsRespVO.java new file mode 100644 index 0000000..9bdf07b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherStatisticsRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 凭证汇总 Response VO") +@Data +@ExcelIgnoreUnannotated +public class FmsVoucherStatisticsRespVO { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long subjectId; + + @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @ExcelProperty("科目编码") + private String subjectCode; + + @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金") + @ExcelProperty("科目名称") + private String subjectName; + + @Schema(description = "科目级次", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer level; + + @Schema(description = "借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @ExcelProperty("借方金额") + private BigDecimal debitAmount; + + @Schema(description = "贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @ExcelProperty("贷方金额") + private BigDecimal creditAmount; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSubjectAmountVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSubjectAmountVO.java new file mode 100644 index 0000000..c173a8a --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSubjectAmountVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * FMS 凭证科目发生额 VO + * + * @author 芋道源码 + */ +@Data +public class FmsVoucherSubjectAmountVO { + + /** + * 科目编号 + */ + private Long subjectId; + /** + * 借方发生额 + */ + private BigDecimal debitAmount; + /** + * 贷方发生额 + */ + private BigDecimal creditAmount; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSubjectBalanceRespVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSubjectBalanceRespVO.java new file mode 100644 index 0000000..fb29d97 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherSubjectBalanceRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - FMS 凭证科目余额 Response VO") +@Data +@Accessors(chain = true) +public class FmsVoucherSubjectBalanceRespVO { + + @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long subjectId; + + @Schema(description = "余额方向", example = "借") + private String balanceDirection; + + @Schema(description = "余额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal balance; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherTidyReqVO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherTidyReqVO.java new file mode 100644 index 0000000..aad2bc7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherTidyReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherTidyTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - FMS 凭证整理 Request VO") +@Data +public class FmsVoucherTidyReqVO { + + @Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账套不能为空") + private Long accountSetId; + + @Schema(description = "整理月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08") + @NotBlank(message = "整理月份不能为空") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "整理月份格式必须为 yyyy-MM") + private String month; + + @Schema(description = "凭证字编号,为空时整理全部凭证字", example = "11") + private Long voucherWordId; + + @Schema(description = "起始编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "起始编号不能为空") + @Min(value = 1, message = "起始编号必须大于 0") + private Integer startNumber; + + @Schema(description = "整理方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "整理方式不能为空") + @InEnum(FmsVoucherTidyTypeEnum.class) + private Integer type; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingPeriodDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingPeriodDO.java new file mode 100644 index 0000000..abeb7d8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingPeriodDO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.closing; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * FMS 结账期间 DO + * + * @author 芋道源码 + */ +@TableName("fms_closing_period") +@KeySequence("fms_closing_period_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsClosingPeriodDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 结账时间 + */ + private LocalDateTime closingTime; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingSchemeDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingSchemeDO.java new file mode 100644 index 0000000..741081d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingSchemeDO.java @@ -0,0 +1,163 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.closing; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * FMS 结账方案 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_closing", autoResultMap = true) +@KeySequence("fms_closing_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsClosingSchemeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 结账名称 + */ + private String name; + /** + * 是否期末结转 + */ + private Boolean periodEnd; + /** + * 科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 取数规则 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum} + */ + private Integer formulaRule; + /** + * 取数时间类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum} + */ + private Integer timeType; + /** + * 凭证字编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO#getId()} + */ + private Long voucherWordId; + /** + * 凭证摘要 + */ + private String digest; + /** + * 结转凭证类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.closing.FmsClosingVoucherTypeEnum} + */ + private Integer voucherType; + /** + * “以前年度损益调整”科目 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long priorYearAdjustmentSubjectId; + /** + * “以前年度损益调整”科目的结转科目 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long adjustmentClosingSubjectId; + /** + * 其他损益科目的结转科目 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long otherClosingSubjectId; + /** + * 是否按余额反向结转 + */ + private Boolean reverseBalance; + /** + * 类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum} + */ + private Integer type; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 结转损益日期,最大31,超过31默认最后一天 + */ + private Integer closingDay; + /** + * 结转科目规则数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List subjectRules; + + /** + * 结转科目规则 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SubjectRule { + + /** + * 科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 科目编码快照 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getCode()} + */ + private String subjectCode; + /** + * 摘要 + */ + private String digest; + /** + * 借贷方向 + * + * 枚举 {@link FmsDebitCreditDirectionEnum} + */ + private Integer direction; + /** + * 金额比例,取值范围 0 至 100 + */ + private BigDecimal amountRatio; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingTemplateDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingTemplateDO.java new file mode 100644 index 0000000..84f8fb5 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingTemplateDO.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.closing; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * FMS 结账模板 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_closing_template", autoResultMap = true) +@KeySequence("fms_closing_template_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsClosingTemplateDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 系统预置编码 + */ + private String presetCode; + /** + * 模板名称 + */ + private String name; + /** + * 模板分类 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTemplateCategoryEnum} + */ + private Integer category; + /** + * 是否期末结转 + */ + private Boolean periodEnd; + /** + * 来源科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 取数规则 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum} + */ + private Integer formulaRule; + /** + * 取数时间类型 + * + * 枚举 {@link FmsClosingTimeTypeEnum} + */ + private Integer timeType; + /** + * 结转科目规则数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List subjectRules; + /** + * 显示顺序 + */ + private Integer sort; + + /** + * 结转科目规则 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SubjectRule { + + /** + * 科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 科目编码快照 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getCode()} + */ + private String subjectCode; + /** + * 摘要 + */ + private String digest; + /** + * 借贷方向 + * + * 枚举 {@link FmsDebitCreditDirectionEnum} + */ + private Integer direction; + /** + * 金额比例,取值范围 0 至 100 + */ + private BigDecimal amountRatio; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingVoucherDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingVoucherDO.java new file mode 100644 index 0000000..af4926e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/closing/FmsClosingVoucherDO.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.closing; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * FMS 结账凭证 DO + * + * @author 芋道源码 + */ +@TableName("fms_closing_voucher") +@KeySequence("fms_closing_voucher_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsClosingVoucherDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 结账方案编号 + * + * 关联 {@link FmsClosingSchemeDO#getId()} + */ + private Long closingId; + /** + * 凭证编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO#getId()} + */ + private Long voucherId; + /** + * 凭证日期 + */ + private LocalDateTime voucherTime; + /** + * 结转金额 + */ + private BigDecimal amount; + /** + * 是否已结账 + */ + private Boolean closed; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAccountSetDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAccountSetDO.java new file mode 100644 index 0000000..3148935 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAccountSetDO.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * FMS 账套 DO + * + * @author 芋道源码 + */ +@TableName("fms_account_set") +@KeySequence("fms_account_set_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsAccountSetDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 公司编码 + */ + private String companyCode; + /** + * 公司名称 + */ + private String companyName; + /** + * 公司简介 + */ + private String companyProfile; + /** + * 所在行业 + */ + private String industry; + /** + * 所在地 + */ + private String location; + /** + * 法人代表 + */ + private String legalRepresentative; + /** + * 身份证号 + */ + private String legalRepresentativeIdNumber; + /** + * 营业执照号 + */ + private String businessLicenseNumber; + /** + * 组织机构代码 + */ + private String organizationCode; + /** + * 备注 + */ + private String remark; + /** + * 联系人 + */ + private String contactName; + /** + * 办公电话 + */ + private String officeTelephone; + /** + * 手机号码 + */ + private String mobile; + /** + * 传真号码 + */ + private String faxNumber; + /** + * QQ 号码 + */ + private String qqNumber; + /** + * 邮箱 + */ + private String email; + /** + * 其他 + */ + private String otherContact; + /** + * 详细地址 + */ + private String address; + /** + * 币种编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO#getId()} + */ + private Long currencyId; + /** + * 启用期间 + */ + private LocalDateTime startTime; + /** + * 会计制度 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsAccountingStandardEnum} + */ + private Integer standard; + /** + * 是否已初始化 + */ + private Boolean initialized; +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAccountUserDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAccountUserDO.java new file mode 100644 index 0000000..03458b5 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAccountUserDO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 账套用户 DO + * + * @author 芋道源码 + */ +@TableName("fms_account_user") +@KeySequence("fms_account_user_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsAccountUserDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 后台用户编号 + * + * 关联 {@link cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO#getId()} + */ + private Long userId; + /** + * 是否默认账套 + */ + private Boolean defaultStatus; + /** + * 是否账套创建人 + */ + private Boolean founder; + /** + * 成员权限级别 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsAccountUserLevelEnum} + */ + private Integer level; +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryCombinationDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryCombinationDO.java new file mode 100644 index 0000000..57a452e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryCombinationDO.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * FMS 辅助核算组合 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_assist_combination", autoResultMap = true) +@KeySequence("fms_assist_combination_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsAuxiliaryCombinationDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 辅助核算项目数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List items; + + /** + * 辅助核算项目 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class AuxiliaryItem { + + /** + * 辅助核算类别 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum} + */ + private Integer type; + /** + * 辅助核算类别编号 + * + * 关联 {@link FmsAuxiliaryTypeDO#getId()} + */ + private Long typeId; + /** + * 辅助核算项目编号 + * + * 关联 {@link FmsAuxiliaryItemDO#getId()} + */ + private Long itemId; + /** + * 辅助核算项目名称快照 + * + * 关联 {@link FmsAuxiliaryItemDO#getName()} + */ + private String name; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryItemDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryItemDO.java new file mode 100644 index 0000000..f211c8c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryItemDO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 辅助核算项目 DO + * + * @author 芋道源码 + */ +@TableName("fms_auxiliary_item") +@KeySequence("fms_auxiliary_item_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsAuxiliaryItemDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 编码 + */ + private String code; + /** + * 名称 + */ + private String name; + /** + * 辅助核算类别编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO#getId()} + */ + private Long auxiliaryTypeId; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 备注 + */ + private String remark; + /** + * 规格(存货) + */ + private String specification; + /** + * 单位(存货) + */ + private String unit; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryTypeDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryTypeDO.java new file mode 100644 index 0000000..c490177 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsAuxiliaryTypeDO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 辅助核算类别 DO + * + * @author 芋道源码 + */ +@TableName("fms_auxiliary_type") +@KeySequence("fms_auxiliary_type_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsAuxiliaryTypeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 辅助核算名称 + */ + private String name; + /** + * 是否系统预置 + */ + private Boolean systemPreset; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum} + */ + private Integer type; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsCurrencyDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsCurrencyDO.java new file mode 100644 index 0000000..c3484a3 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsCurrencyDO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * FMS 币别 DO + * + * @author 芋道源码 + */ +@TableName("fms_currency") +@KeySequence("fms_currency_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsCurrencyDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 编码 + */ + private String code; + /** + * 币种名称 + */ + private String name; + /** + * 汇率 + */ + private BigDecimal exchangeRate; + /** + * 是否本位币 + */ + private Boolean standard; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsDigestDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsDigestDO.java new file mode 100644 index 0000000..22c3abd --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsDigestDO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * FMS 常用摘要 DO + * + * @author 芋道源码 + */ +@TableName("fms_digest") +@KeySequence("fms_digest_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsDigestDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 摘要内容 + */ + private String content; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsFinanceIndicatorDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsFinanceIndicatorDO.java new file mode 100644 index 0000000..8f516cf --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsFinanceIndicatorDO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +// TODO DONE @AI:DO 字段注释、枚举引用和状态字段已对齐 FMS 现有 DO 风格。 +/** + * FMS 首页财务指标 DO + * + * @author 芋道源码 + */ +@TableName("fms_finance_indicator") +@KeySequence("fms_finance_indicator_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsFinanceIndicatorDO extends BaseDO { + + // TODO DONE @AI:报表类型常量已迁移到 FmsFinanceIndicatorTypeEnum。 + /** + * 编号 + */ + @TableId + private Long id; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 指标名称 + */ + private String name; + /** + * 指标编码 + */ + private String code; + /** + * 取数报表类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsFinanceIndicatorTypeEnum} + */ + private Integer type; + /** + * 指标公式,支持报表行次公式或科目公式 JSON + */ + private String formula; + /** + * 展示顺序 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsFinanceParameterDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsFinanceParameterDO.java new file mode 100644 index 0000000..87737c8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsFinanceParameterDO.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 财务参数 DO + * + * @author 芋道源码 + */ +@TableName("fms_finance_parameter") +@KeySequence("fms_finance_parameter_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsFinanceParameterDO extends BaseDO { + + public static final Integer DEFAULT_LEVEL = 4; + + public static final String DEFAULT_SUBJECT_CODE_RULE = "4-2-2-2"; + + public static final Integer DEFAULT_LEDGER_BALANCE_MODE = FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode(); + public static final Boolean DEFAULT_VOUCHER_REVIEW_REQUIRED = false; + public static final Boolean DEFAULT_DEFICIT_CHECK = false; + public static final Boolean DEFAULT_ASSET_PERIOD_LOCKED = false; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 科目层级 + */ + private Integer level; + /** + * 科目编码规则 + */ + private String subjectCodeRule; + /** + * 账簿余额方向模式 + */ + private Integer ledgerBalanceMode; + /** + * 是否检查现金及银行存款科目赤字 + */ + private Boolean deficitCheck; + /** + * 结账前是否要求凭证审核 + */ + private Boolean voucherReviewRequired; + /** + * 生成折旧凭证后是否锁定以前期间资产卡片 + */ + private Boolean assetPeriodLocked; + /** + * 纳税人名称 + */ + private String taxpayerName; + /** + * 纳税人识别号 + */ + private String taxpayerNumber; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsInitialBalanceDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsInitialBalanceDO.java new file mode 100644 index 0000000..fef8c60 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsInitialBalanceDO.java @@ -0,0 +1,197 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * FMS 科目期初余额 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_initial_balance", autoResultMap = true) +@KeySequence("fms_initial_balance_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsInitialBalanceDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 是否启用辅助核算 + */ + private Boolean auxiliaryAccounting; + /** + * 期初金额 + */ + private BigDecimal openingAmount; + /** + * 期初数量 + */ + private BigDecimal openingQuantity; + /** + * 本年累计借方金额 + */ + private BigDecimal yearDebitAmount; + /** + * 本年累计借方数量 + */ + private BigDecimal yearDebitQuantity; + /** + * 本年累计贷方金额 + */ + private BigDecimal yearCreditAmount; + /** + * 本年累计贷方数量 + */ + private BigDecimal yearCreditQuantity; + /** + * 年初金额 + */ + private BigDecimal yearOpeningAmount; + /** + * 年初数量 + */ + private BigDecimal yearOpeningQuantity; + /** + * 实际损益发生额 + */ + private BigDecimal profitLossAmount; + /** + * 实际损益发生数量 + */ + private BigDecimal profitLossQuantity; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 辅助核算余额数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List assistBalances; + + /** + * 辅助核算余额 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class AssistBalance { + + /** + * 辅助核算组合编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO#getId()} + */ + private Long assistCombinationId; + /** + * 辅助核算项目数组 + */ + private List auxiliaries; + /** + * 期初金额 + */ + private BigDecimal openingAmount; + /** + * 期初数量 + */ + private BigDecimal openingQuantity; + /** + * 本年累计借方金额 + */ + private BigDecimal yearDebitAmount; + /** + * 本年累计借方数量 + */ + private BigDecimal yearDebitQuantity; + /** + * 本年累计贷方金额 + */ + private BigDecimal yearCreditAmount; + /** + * 本年累计贷方数量 + */ + private BigDecimal yearCreditQuantity; + /** + * 年初金额 + */ + private BigDecimal yearOpeningAmount; + /** + * 年初数量 + */ + private BigDecimal yearOpeningQuantity; + /** + * 实际损益发生额 + */ + private BigDecimal profitLossAmount; + /** + * 实际损益发生数量 + */ + private BigDecimal profitLossQuantity; + } + + /** + * 辅助核算项目 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class AuxiliaryItem { + + /** + * 辅助核算类别 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum} + */ + private Integer type; + /** + * 辅助核算类别编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO#getId()} + */ + private Long typeId; + /** + * 辅助核算项目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO#getId()} + */ + private Long itemId; + /** + * 辅助核算项目名称快照 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO#getName()} + */ + private String name; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsSubjectDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsSubjectDO.java new file mode 100644 index 0000000..cff165d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsSubjectDO.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * FMS 会计科目 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_subject", autoResultMap = true) +@KeySequence("fms_subject_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsSubjectDO extends BaseDO { + + /** + * 根科目编号 + */ + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 科目编码 + */ + private String code; + /** + * 科目名称 + */ + private String name; + /** + * 上级科目编号 + * + * 关联 {@link FmsSubjectDO#getId()} + */ + private Long parentId; + /** + * 科目类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum} + */ + private Integer type; + /** + * 科目类别 + */ + private Integer category; + /** + * 余额方向 + * + * 枚举 {@link FmsDebitCreditDirectionEnum} + */ + private Integer balanceDirection; + /** + * 数量核算 计量单位 + */ + private String quantityUnit; + /** + * 是否现金科目 + */ + private Boolean cash; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 科目层级 + */ + private Integer level; + /** + * 是否启用数量核算 + */ + private Boolean quantityAccounting; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 辅助核算类别编号数组 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List auxiliaryTypeIds; + /** + * 核算币种编号数组 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List currencyIds; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsSubjectTemplateDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsSubjectTemplateDO.java new file mode 100644 index 0000000..7c9fcf1 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsSubjectTemplateDO.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 科目模板 DO + * + * @author 芋道源码 + */ +@TableName("fms_subject_template") +@KeySequence("fms_subject_template_seq") +@TenantIgnore +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsSubjectTemplateDO extends BaseDO { + + /** + * 根科目编号 + */ + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 科目编码 + */ + private String code; + /** + * 科目名称 + */ + private String name; + /** + * 上级科目编号 + * + * 关联 {@link FmsSubjectTemplateDO#getId()} + */ + private Long parentId; + /** + * 科目类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum} + */ + private Integer type; + /** + * 科目类别 + */ + private Integer category; + /** + * 余额方向 + * + * 枚举 {@link FmsDebitCreditDirectionEnum} + */ + private Integer balanceDirection; + /** + * 数量核算 计量单位 + */ + private String quantityUnit; + /** + * 是否现金科目 + */ + private Boolean cash; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 科目层级 + */ + private Integer level; + /** + * 是否启用数量核算 + */ + private Boolean quantityAccounting; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherTemplateCategoryDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherTemplateCategoryDO.java new file mode 100644 index 0000000..c9959cb --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherTemplateCategoryDO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * FMS 凭证模板分类 DO + * + * @author 芋道源码 + */ +@TableName("fms_voucher_template_category") +@KeySequence("fms_voucher_template_category_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsVoucherTemplateCategoryDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 凭证模板分类名称 + */ + private String name; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherTemplateDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherTemplateDO.java new file mode 100644 index 0000000..09793a1 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherTemplateDO.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * FMS 凭证模板 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_voucher_template", autoResultMap = true) +@KeySequence("fms_voucher_template_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsVoucherTemplateDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 凭证模板名称 + */ + private String name; + /** + * 凭证模板分类编号 + * + * 关联 {@link FmsVoucherTemplateCategoryDO#getId()} + */ + private Long categoryId; + /** + * 凭证模板分录数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List entries; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + + /** + * 凭证模板分录 + */ + @Data + public static class Entry { + + /** + * 摘要内容 + */ + private String digest; + /** + * 科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 数量 + */ + private BigDecimal quantity; + /** + * 单价 + */ + private BigDecimal unitPrice; + /** + * 借方金额 + */ + private BigDecimal debitAmount; + /** + * 贷方金额 + */ + private BigDecimal creditAmount; + /** + * 辅助核算项目数组 + */ + private List auxiliaries; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherWordDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherWordDO.java new file mode 100644 index 0000000..4387175 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/config/FmsVoucherWordDO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * FMS 凭证字 DO + * + * @author 芋道源码 + */ +@TableName("fms_voucher_word") +@KeySequence("fms_voucher_word_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FmsVoucherWordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 凭证字 + */ + private String name; + /** + * 打印标题 + */ + private String printTitle; + /** + * 是否默认凭证字 + */ + private Boolean defaultStatus; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/FmsReportTemplateDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/FmsReportTemplateDO.java new file mode 100644 index 0000000..31cb820 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/FmsReportTemplateDO.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 报表模板 DO + * + * @author 芋道源码 + */ +@TableName("fms_report_template") +@KeySequence("fms_report_template_seq") +@TenantIgnore +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsReportTemplateDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 行次 + */ + private Integer rowNo; + /** + * 公式 + */ + private String formula; + /** + * 备注 + */ + private String remark; + /** + * 是否可编辑 + */ + private Boolean editable; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 行编号 + */ + private Integer rowId; + /** + * 类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum} + */ + private Integer type; + /** + * 现金流量表分类 + */ + private Integer category; + /** + * 层级 + */ + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/balance/FmsBalanceSheetConfigDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/balance/FmsBalanceSheetConfigDO.java new file mode 100644 index 0000000..5405a37 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/balance/FmsBalanceSheetConfigDO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.balance; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 资产负债表配置 DO + * + * @author 芋道源码 + */ +@TableName("fms_balance_sheet_config") +@KeySequence("fms_balance_sheet_config_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsBalanceSheetConfigDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 行次 + */ + private Integer rowNo; + /** + * 公式 + */ + private String formula; + /** + * 备注 + */ + private String remark; + /** + * 是否可编辑 + */ + private Boolean editable; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 层级 + */ + private Integer level; + /** + * 行编号 + */ + private Integer rowId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/balance/FmsBalanceSheetReportDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/balance/FmsBalanceSheetReportDO.java new file mode 100644 index 0000000..806048e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/balance/FmsBalanceSheetReportDO.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.balance; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * FMS 资产负债表数据 DO + * + * @author 芋道源码 + */ +@TableName("fms_balance_sheet_report") +@KeySequence("fms_balance_sheet_report_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsBalanceSheetReportDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 开始会计期间,格式为 yyyyMM + */ + private Integer fromPeriod; + /** + * 结束会计期间,格式为 yyyyMM + */ + private Integer toPeriod; + /** + * 报表期间类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsReportPeriodTypeEnum} + */ + private Integer type; + /** + * 层级快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getLevel()},用于保留历史期间的展示层级 + */ + private Integer level; + /** + * 配置名称快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getName()},用于保留历史期间的项目名称 + */ + private String name; + /** + * 行次快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getRowNo()},用于保留历史期间的报表行次 + */ + private Integer rowNo; + /** + * 公式快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getFormula()},保证历史期间报表不受后续公式调整影响 + */ + private String formula; + /** + * 备注快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getRemark()},用于保留历史期间的备注 + */ + private String remark; + /** + * 是否可编辑快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getEditable()},用于保留历史期间的可编辑标识 + */ + private Boolean editable; + /** + * 显示顺序快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getSort()},用于保留历史期间的显示顺序 + */ + private Integer sort; + /** + * 期初金额 + */ + private BigDecimal openingAmount; + /** + * 期末金额 + */ + private BigDecimal closingAmount; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 是否已结账 + */ + private Boolean settled; + /** + * 行编号快照 + * + * 冗余 {@link FmsBalanceSheetConfigDO#getRowId()},用于保留历史期间资产与负债项目的行配对关系 + */ + private Integer rowId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowExtendConfigDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowExtendConfigDO.java new file mode 100644 index 0000000..0a0a6fd --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowExtendConfigDO.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * FMS 现金流量表扩展配置 DO + * + * @author 芋道源码 + */ +@TableName("fms_cash_flow_extend_config") +@KeySequence("fms_cash_flow_extend_config_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsCashFlowExtendConfigDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 行次 + */ + private Integer rowNo; + /** + * 公式 + */ + private String formula; + /** + * 备注 + */ + private String remark; + /** + * 表格标识 + */ + private Integer category; + /** + * 公式类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsCashFlowExtendTypeEnum} + */ + private Integer type; + /** + * 本期金额 + */ + private BigDecimal currentAmount; + /** + * 本年累计金额 + */ + private BigDecimal yearAmount; + /** + * 是否可编辑 + */ + private Boolean editable; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 层级 + */ + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowExtendDataDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowExtendDataDO.java new file mode 100644 index 0000000..78a9a2b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowExtendDataDO.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * FMS 现金流量表扩展数据 DO + * + * @author 芋道源码 + */ +@TableName("fms_cash_flow_extend_data") +@KeySequence("fms_cash_flow_extend_data_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsCashFlowExtendDataDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 行次 + */ + private Integer rowNo; + /** + * 公式 + */ + private String formula; + /** + * 备注 + */ + private String remark; + /** + * 现金流量表分类 + */ + private Integer category; + /** + * 本期金额 + */ + private BigDecimal currentAmount; + /** + * 本年累计金额 + */ + private BigDecimal yearAmount; + /** + * 开始会计期间,格式为 yyyyMM + */ + private Integer fromPeriod; + /** + * 是否可编辑 + */ + private Boolean editable; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 结束会计期间,格式为 yyyyMM + */ + private Integer toPeriod; + /** + * 报表期间类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsReportPeriodTypeEnum} + */ + private Integer type; + /** + * 层级 + */ + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowStatementConfigDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowStatementConfigDO.java new file mode 100644 index 0000000..a9d87bc --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowStatementConfigDO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 现金流量表配置 DO + * + * @author 芋道源码 + */ +@TableName("fms_cash_flow_statement_config") +@KeySequence("fms_cash_flow_statement_config_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsCashFlowStatementConfigDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 行次 + */ + private Integer rowNo; + /** + * 公式 + */ + private String formula; + /** + * 备注 + */ + private String remark; + /** + * 是否可编辑 + */ + private Boolean editable; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 现金流量表分类 + */ + private Integer category; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 层级 + */ + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowStatementReportDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowStatementReportDO.java new file mode 100644 index 0000000..464529f --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/cashflow/FmsCashFlowStatementReportDO.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * FMS 现金流量表数据 DO + * + * 按会计期间保存报表配置的快照:名称、行次、公式、是否可编辑、显示顺序和层级冗余自 {@link FmsCashFlowStatementConfigDO},保证历史期间报表保留当时的公式,不受后续公式调整影响 + * + * @author 芋道源码 + */ +@TableName("fms_cash_flow_statement_report") +@KeySequence("fms_cash_flow_statement_report_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsCashFlowStatementReportDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 开始会计期间,格式为 yyyyMM + */ + private Integer fromPeriod; + /** + * 配置名称 + * + * 冗余 {@link FmsCashFlowStatementConfigDO#getName()} + */ + private String name; + /** + * 行次 + * + * 冗余 {@link FmsCashFlowStatementConfigDO#getRowNo()} + */ + private Integer rowNo; + /** + * 公式 + * + * 冗余 {@link FmsCashFlowStatementConfigDO#getFormula()} + */ + private String formula; + /** + * 备注 + */ + private String remark; + /** + * 是否可编辑 + * + * 冗余 {@link FmsCashFlowStatementConfigDO#getEditable()} + */ + private Boolean editable; + /** + * 本期金额 + */ + private BigDecimal currentAmount; + /** + * 本年累计金额 + */ + private BigDecimal yearAmount; + /** + * 显示顺序 + * + * 冗余 {@link FmsCashFlowStatementConfigDO#getSort()} + */ + private Integer sort; + /** + * 现金流量表分类 + */ + private Integer category; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 结束会计期间,格式为 yyyyMM + */ + private Integer toPeriod; + /** + * 报表期间类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsReportPeriodTypeEnum} + */ + private Integer type; + /** + * 层级 + * + * 冗余 {@link FmsCashFlowStatementConfigDO#getLevel()} + */ + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/income/FmsIncomeStatementConfigDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/income/FmsIncomeStatementConfigDO.java new file mode 100644 index 0000000..c1f3103 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/income/FmsIncomeStatementConfigDO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.income; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * FMS 利润表配置 DO + * + * @author 芋道源码 + */ +@TableName("fms_income_statement_config") +@KeySequence("fms_income_statement_config_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsIncomeStatementConfigDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 行次 + */ + private Integer rowNo; + /** + * 公式 + */ + private String formula; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 是否可编辑 + */ + private Boolean editable; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 层级 + */ + private Integer level; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/income/FmsIncomeStatementReportDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/income/FmsIncomeStatementReportDO.java new file mode 100644 index 0000000..be60112 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/report/income/FmsIncomeStatementReportDO.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.report.income; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * FMS 利润表数据 DO + * + * 按会计期间保存报表配置的快照:名称、行次、公式、显示顺序、是否可编辑和层级冗余自 {@link FmsIncomeStatementConfigDO},保证历史期间报表保留当时的公式,不受后续公式调整影响 + * + * @author 芋道源码 + */ +@TableName("fms_income_statement_report") +@KeySequence("fms_income_statement_report_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsIncomeStatementReportDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 报表期间类型 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.report.FmsReportPeriodTypeEnum} + */ + private Integer type; + /** + * 开始会计期间,格式为 yyyyMM + */ + private Integer fromPeriod; + /** + * 结束会计期间,格式为 yyyyMM + */ + private Integer toPeriod; + /** + * 配置名称 + * + * 冗余 {@link FmsIncomeStatementConfigDO#getName()} + */ + private String name; + /** + * 行次 + * + * 冗余 {@link FmsIncomeStatementConfigDO#getRowNo()} + */ + private Integer rowNo; + /** + * 公式 + * + * 冗余 {@link FmsIncomeStatementConfigDO#getFormula()} + */ + private String formula; + /** + * 显示顺序 + * + * 冗余 {@link FmsIncomeStatementConfigDO#getSort()} + */ + private Integer sort; + /** + * 是否可编辑 + * + * 冗余 {@link FmsIncomeStatementConfigDO#getEditable()} + */ + private Boolean editable; + /** + * 本期金额 + */ + private BigDecimal currentAmount; + /** + * 本年累计金额 + */ + private BigDecimal yearAmount; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 层级 + * + * 冗余 {@link FmsIncomeStatementConfigDO#getLevel()} + */ + private Integer level; + /** + * 是否已结账 + */ + private Boolean settled; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/voucher/FmsVoucherDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/voucher/FmsVoucherDO.java new file mode 100644 index 0000000..e540166 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/voucher/FmsVoucherDO.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.voucher; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * FMS 凭证 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_voucher", autoResultMap = true) +@KeySequence("fms_voucher_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsVoucherDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 凭证字编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO#getId()} + */ + private Long voucherWordId; + /** + * 凭证号 + */ + private Integer voucherNumber; + /** + * 凭证日期 + */ + private LocalDateTime voucherTime; + /** + * 附件地址 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List attachmentUrls; + /** + * 附单据张数 + */ + private Integer attachmentCount; + /** + * 借方金额 + */ + private BigDecimal debitAmount; + /** + * 贷方金额 + */ + private BigDecimal creditAmount; + /** + * 合计金额 + */ + private BigDecimal total; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherStatusEnum} + */ + private Integer status; + /** + * 审核人后台用户编号 + * + * 关联 {@link cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO#getId()} + */ + private Long reviewerUserId; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/voucher/FmsVoucherEntryDO.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/voucher/FmsVoucherEntryDO.java new file mode 100644 index 0000000..9ae812b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/dataobject/voucher/FmsVoucherEntryDO.java @@ -0,0 +1,141 @@ +package cn.iocoder.yudao.module.fms.dal.dataobject.voucher; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * FMS 凭证分录 DO + * + * @author 芋道源码 + */ +@TableName(value = "fms_voucher_entry", autoResultMap = true) +@KeySequence("fms_voucher_entry_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FmsVoucherEntryDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 摘要内容 + */ + private String digest; + /** + * 科目名称快照 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getName()} + */ + private String subjectName; + /** + * 数量 + */ + private BigDecimal quantity; + /** + * 借方金额 + */ + private BigDecimal debitAmount; + /** + * 贷方金额 + */ + private BigDecimal creditAmount; + /** + * 凭证编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO#getId()} + */ + private Long voucherId; + /** + * 科目编码快照 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getCode()} + */ + private String subjectCode; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 科目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO#getId()} + */ + private Long subjectId; + /** + * 账套编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO#getId()} + */ + private Long accountSetId; + /** + * 单价 + */ + private BigDecimal unitPrice; + /** + * 辅助核算组合编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO#getId()} + */ + private Long assistCombinationId; + /** + * 辅助核算项目数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List auxiliaries; + + /** + * 辅助核算项目 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class AuxiliaryItem { + + /** + * 辅助核算类别 + * + * 枚举 {@link cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum} + */ + private Integer type; + /** + * 辅助核算类别编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO#getId()} + */ + private Long typeId; + /** + * 辅助核算项目编号 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO#getId()} + */ + private Long itemId; + /** + * 辅助核算项目名称快照 + * + * 关联 {@link cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO#getName()} + */ + private String name; + + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingPeriodMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingPeriodMapper.java new file mode 100644 index 0000000..a6675c2 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingPeriodMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.closing; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingPeriodDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; + +/** + * FMS 结账期间 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsClosingPeriodMapper extends BaseMapperX { + + default FmsClosingPeriodDO selectByPeriod(Long accountSetId, + LocalDateTime beginTime, LocalDateTime endTime) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsClosingPeriodDO::getAccountSetId, accountSetId) + .between(FmsClosingPeriodDO::getClosingTime, beginTime, endTime)); + } + + default FmsClosingPeriodDO selectLatestByAccountSetId(Long accountSetId) { + return selectLastOne(new LambdaQueryWrapperX() + .eq(FmsClosingPeriodDO::getAccountSetId, accountSetId) + .orderByAsc(FmsClosingPeriodDO::getClosingTime) + .orderByAsc(FmsClosingPeriodDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingSchemeMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingSchemeMapper.java new file mode 100644 index 0000000..7066f35 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingSchemeMapper.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.closing; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 结账方案 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsClosingSchemeMapper extends BaseMapperX { + + default FmsClosingSchemeDO selectByAccountSetIdAndType(Long accountSetId, Integer type) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsClosingSchemeDO::getAccountSetId, accountSetId) + .eq(FmsClosingSchemeDO::getType, type)); + } + + default FmsClosingSchemeDO selectByIdAndAccountSetId(Long id, Long accountSetId) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsClosingSchemeDO::getId, id) + .eq(FmsClosingSchemeDO::getAccountSetId, accountSetId)); + } + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsClosingSchemeDO::getAccountSetId, accountSetId) + .orderByAsc(FmsClosingSchemeDO::getType) + .orderByAsc(FmsClosingSchemeDO::getId)); + } + + default List selectPeriodEndListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsClosingSchemeDO::getAccountSetId, accountSetId) + .and(wrapper -> wrapper.ne(FmsClosingSchemeDO::getType, + FmsClosingTypeEnum.REGULAR.getType()) + .or().eq(FmsClosingSchemeDO::getPeriodEnd, true)) + .orderByAsc(FmsClosingSchemeDO::getType) + .orderByAsc(FmsClosingSchemeDO::getId)); + } + + default Long selectCountByAccountSetIdAndVoucherWordId( + Long accountSetId, Long voucherWordId) { + return selectCount(new LambdaQueryWrapperX() + .eq(FmsClosingSchemeDO::getAccountSetId, accountSetId) + .eq(FmsClosingSchemeDO::getVoucherWordId, voucherWordId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingTemplateMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingTemplateMapper.java new file mode 100644 index 0000000..5f13b90 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingTemplateMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.closing; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 结账模板 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsClosingTemplateMapper extends BaseMapperX { + + default FmsClosingTemplateDO selectByIdAndAccountSetId(Long id, Long accountSetId) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsClosingTemplateDO::getId, id) + .eq(FmsClosingTemplateDO::getAccountSetId, accountSetId)); + } + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsClosingTemplateDO::getAccountSetId, accountSetId) + .orderByAsc(FmsClosingTemplateDO::getCategory) + .orderByAsc(FmsClosingTemplateDO::getSort) + .orderByAsc(FmsClosingTemplateDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingVoucherMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingVoucherMapper.java new file mode 100644 index 0000000..5677a92 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/closing/FmsClosingVoucherMapper.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.closing; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +/** + * FMS 结账凭证 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsClosingVoucherMapper extends BaseMapperX { + + default List selectListByPeriod(Long accountSetId, + LocalDateTime beginTime, LocalDateTime endTime) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsClosingVoucherDO::getAccountSetId, accountSetId) + .between(FmsClosingVoucherDO::getVoucherTime, beginTime, endTime) + .orderByDesc(FmsClosingVoucherDO::getId)); + } + + default List selectListByClosingId(Long closingId) { + return selectList(FmsClosingVoucherDO::getClosingId, closingId); + } + + default Long selectCountByClosingId(Long closingId) { + return selectCount(FmsClosingVoucherDO::getClosingId, closingId); + } + + default List selectListByClosingIdAndPeriod(Long closingId, + LocalDateTime beginTime, LocalDateTime endTime) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsClosingVoucherDO::getClosingId, closingId) + .between(FmsClosingVoucherDO::getVoucherTime, beginTime, endTime) + .orderByDesc(FmsClosingVoucherDO::getId)); + } + + default List selectListByClosingIdAndVoucherTimeBetween(Long closingId, + LocalDateTime beginTime, LocalDateTime endTime) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsClosingVoucherDO::getClosingId, closingId) + .geIfPresent(FmsClosingVoucherDO::getVoucherTime, beginTime) + .ltIfPresent(FmsClosingVoucherDO::getVoucherTime, endTime) + .orderByAsc(FmsClosingVoucherDO::getVoucherTime) + .orderByAsc(FmsClosingVoucherDO::getId)); + } + + default List selectListByAccountSetIdAndVoucherIds( + Long accountSetId, Collection voucherIds) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsClosingVoucherDO::getAccountSetId, accountSetId) + .in(FmsClosingVoucherDO::getVoucherId, voucherIds)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAccountSetMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAccountSetMapper.java new file mode 100644 index 0000000..38fa6d9 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAccountSetMapper.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * FMS 账套 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsAccountSetMapper extends BaseMapperX { + + default FmsAccountSetDO selectByCompanyCode(String companyCode) { + return selectOne(FmsAccountSetDO::getCompanyCode, companyCode); + } + + default FmsAccountSetDO selectByIdForUpdate(Long id) { + return selectOneForUpdate(FmsAccountSetDO::getId, id); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAccountUserMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAccountUserMapper.java new file mode 100644 index 0000000..e6005a4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAccountUserMapper.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 账套用户 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsAccountUserMapper extends BaseMapperX { + + default FmsAccountUserDO selectByAccountSetIdAndUserId(Long accountSetId, Long userId) { + return selectOne(FmsAccountUserDO::getAccountSetId, accountSetId, + FmsAccountUserDO::getUserId, userId); + } + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsAccountUserDO::getUserId, userId) + .orderByDesc(FmsAccountUserDO::getDefaultStatus) + .orderByDesc(FmsAccountUserDO::getId)); + } + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsAccountUserDO::getAccountSetId, accountSetId) + .orderByDesc(FmsAccountUserDO::getFounder) + .orderByAsc(FmsAccountUserDO::getId)); + } + + default void deleteByAccountSetIdAndUserIds(Long accountSetId, Collection userIds) { + delete(new LambdaQueryWrapperX() + .eq(FmsAccountUserDO::getAccountSetId, accountSetId) + .in(FmsAccountUserDO::getUserId, userIds)); + } + + default Long selectCountByUserId(Long userId) { + return selectCount(FmsAccountUserDO::getUserId, userId); + } + + default void updateDefaultStatusByUserId(Long userId) { + update(new FmsAccountUserDO().setDefaultStatus(false), + new LambdaQueryWrapperX().eq(FmsAccountUserDO::getUserId, userId)); + } + + default void updateDefaultStatusByAccountSetIdAndUserId(Long accountSetId, Long userId) { + update(new FmsAccountUserDO().setDefaultStatus(true), new LambdaQueryWrapperX() + .eq(FmsAccountUserDO::getAccountSetId, accountSetId).eq(FmsAccountUserDO::getUserId, userId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryCombinationMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryCombinationMapper.java new file mode 100644 index 0000000..c1935a9 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryCombinationMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 辅助核算组合 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsAuxiliaryCombinationMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryCombinationDO::getAccountSetId, accountSetId)); + } + + default Long selectCountByAccountSetIdAndSubjectIds( + Long accountSetId, Collection subjectIds) { + return selectCount(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryCombinationDO::getAccountSetId, accountSetId) + .in(FmsAuxiliaryCombinationDO::getSubjectId, subjectIds)); + } + + default void updateSubject(Long accountSetId, Long subjectId, FmsAuxiliaryCombinationDO updateObj) { + update(updateObj, new LambdaUpdateWrapper() + .eq(FmsAuxiliaryCombinationDO::getAccountSetId, accountSetId) + .eq(FmsAuxiliaryCombinationDO::getSubjectId, subjectId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryItemMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryItemMapper.java new file mode 100644 index 0000000..8f8bad8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryItemMapper.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemPageReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 辅助核算项目 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsAuxiliaryItemMapper extends BaseMapperX { + + default PageResult selectPage(FmsAuxiliaryItemPageReqVO reqVO) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eq(FmsAuxiliaryItemDO::getAccountSetId, reqVO.getAccountSetId()) + .eq(FmsAuxiliaryItemDO::getAuxiliaryTypeId, reqVO.getAuxiliaryTypeId()); + if (StrUtil.isNotBlank(reqVO.getSearch())) { + query.and(wrapper -> wrapper.like(FmsAuxiliaryItemDO::getCode, reqVO.getSearch()) + .or().like(FmsAuxiliaryItemDO::getName, reqVO.getSearch())); + } + return selectPage(reqVO, query.orderByDesc(FmsAuxiliaryItemDO::getCode) + .orderByDesc(FmsAuxiliaryItemDO::getId)); + } + + default FmsAuxiliaryItemDO selectByTypeIdAndCode(Long accountSetId, Long auxiliaryTypeId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryItemDO::getAccountSetId, accountSetId) + .eq(FmsAuxiliaryItemDO::getAuxiliaryTypeId, auxiliaryTypeId) + .eq(FmsAuxiliaryItemDO::getCode, code)); + } + + default Long selectCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId) { + return selectCount(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryItemDO::getAccountSetId, accountSetId) + .eq(FmsAuxiliaryItemDO::getAuxiliaryTypeId, auxiliaryTypeId)); + } + + default List selectListByIdsAndAccountSetId(Collection ids, Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .in(FmsAuxiliaryItemDO::getId, ids) + .eq(FmsAuxiliaryItemDO::getAccountSetId, accountSetId)); + } + + default List selectListByAccountSetIdAndAuxiliaryTypeId( + Long accountSetId, Long auxiliaryTypeId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryItemDO::getAccountSetId, accountSetId) + .eq(FmsAuxiliaryItemDO::getAuxiliaryTypeId, auxiliaryTypeId) + .orderByDesc(FmsAuxiliaryItemDO::getCode) + .orderByDesc(FmsAuxiliaryItemDO::getId)); + } + + default List selectListByAccountSetIdAndAuxiliaryTypeIdAndStatus( + Long accountSetId, Long auxiliaryTypeId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryItemDO::getAccountSetId, accountSetId) + .eq(FmsAuxiliaryItemDO::getAuxiliaryTypeId, auxiliaryTypeId) + .eq(FmsAuxiliaryItemDO::getStatus, status) + .orderByDesc(FmsAuxiliaryItemDO::getCode) + .orderByDesc(FmsAuxiliaryItemDO::getId)); + } + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryItemDO::getAccountSetId, accountSetId) + .orderByAsc(FmsAuxiliaryItemDO::getAuxiliaryTypeId) + .orderByDesc(FmsAuxiliaryItemDO::getCode) + .orderByDesc(FmsAuxiliaryItemDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryTypeMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryTypeMapper.java new file mode 100644 index 0000000..f1aaa45 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsAuxiliaryTypeMapper.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 辅助核算类别 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsAuxiliaryTypeMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryTypeDO::getAccountSetId, accountSetId) + .orderByAsc(FmsAuxiliaryTypeDO::getType) + .orderByAsc(FmsAuxiliaryTypeDO::getId)); + } + + default List selectListByIdsAndAccountSetId( + Collection ids, Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .in(FmsAuxiliaryTypeDO::getId, ids) + .eq(FmsAuxiliaryTypeDO::getAccountSetId, accountSetId)); + } + + default FmsAuxiliaryTypeDO selectByAccountSetIdAndName(Long accountSetId, String name) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsAuxiliaryTypeDO::getAccountSetId, accountSetId) + .eq(FmsAuxiliaryTypeDO::getName, name)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsCurrencyMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsCurrencyMapper.java new file mode 100644 index 0000000..23c5b33 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsCurrencyMapper.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 币种 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsCurrencyMapper extends BaseMapperX { + + default FmsCurrencyDO selectByAccountSetIdAndCode(Long accountSetId, String code) { + return selectOne(FmsCurrencyDO::getAccountSetId, accountSetId, + FmsCurrencyDO::getCode, code); + } + + default FmsCurrencyDO selectByIdAndAccountSetId(Long id, Long accountSetId) { + return selectOne(FmsCurrencyDO::getId, id, + FmsCurrencyDO::getAccountSetId, accountSetId); + } + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsCurrencyDO::getAccountSetId, accountSetId) + .orderByDesc(FmsCurrencyDO::getStandard) + .orderByAsc(FmsCurrencyDO::getId)); + } + + default List selectListByIdsAndAccountSetId( + Collection ids, Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .in(FmsCurrencyDO::getId, ids) + .eq(FmsCurrencyDO::getAccountSetId, accountSetId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsDigestMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsDigestMapper.java new file mode 100644 index 0000000..c63e7cc --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsDigestMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsDigestDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 常用摘要 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsDigestMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsDigestDO::getAccountSetId, accountSetId) + .orderByDesc(FmsDigestDO::getUpdateTime) + .orderByDesc(FmsDigestDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsFinanceIndicatorMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsFinanceIndicatorMapper.java new file mode 100644 index 0000000..e6eab75 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsFinanceIndicatorMapper.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceIndicatorDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 首页财务指标 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsFinanceIndicatorMapper extends BaseMapperX { + + default FmsFinanceIndicatorDO selectByIdAndAccountSetId(Long id, Long accountSetId) { + return selectOne(FmsFinanceIndicatorDO::getId, id, + FmsFinanceIndicatorDO::getAccountSetId, accountSetId); + } + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsFinanceIndicatorDO::getAccountSetId, accountSetId) + .orderByAsc(FmsFinanceIndicatorDO::getSort) + .orderByAsc(FmsFinanceIndicatorDO::getId)); + } + + // TODO DONE @AI:启用指标查询改为按 status 查询,方法名完整表达过滤条件。 + default List selectListByAccountSetIdAndStatus(Long accountSetId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsFinanceIndicatorDO::getAccountSetId, accountSetId) + .eq(FmsFinanceIndicatorDO::getStatus, status) + .orderByAsc(FmsFinanceIndicatorDO::getSort) + .orderByAsc(FmsFinanceIndicatorDO::getId)); + } + + default FmsFinanceIndicatorDO selectByAccountSetIdAndCode(Long accountSetId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsFinanceIndicatorDO::getAccountSetId, accountSetId) + .eq(FmsFinanceIndicatorDO::getCode, code)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsFinanceParameterMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsFinanceParameterMapper.java new file mode 100644 index 0000000..036c613 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsFinanceParameterMapper.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * FMS 财务参数 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsFinanceParameterMapper extends BaseMapperX { + + default FmsFinanceParameterDO selectByAccountSetId(Long accountSetId) { + return selectOne(FmsFinanceParameterDO::getAccountSetId, accountSetId); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsInitialBalanceMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsInitialBalanceMapper.java new file mode 100644 index 0000000..2f4f8b8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsInitialBalanceMapper.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsInitialBalanceDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 科目期初余额 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsInitialBalanceMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsInitialBalanceDO::getAccountSetId, accountSetId) + .orderByAsc(FmsInitialBalanceDO::getSubjectId) + .orderByAsc(FmsInitialBalanceDO::getId)); + } + + default FmsInitialBalanceDO selectByAccountSetIdAndSubjectId(Long accountSetId, Long subjectId) { + return selectOne(FmsInitialBalanceDO::getAccountSetId, accountSetId, + FmsInitialBalanceDO::getSubjectId, subjectId); + } + + default List selectListByAccountSetIdAndSubjectIds( + Long accountSetId, Collection subjectIds) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsInitialBalanceDO::getAccountSetId, accountSetId) + .in(FmsInitialBalanceDO::getSubjectId, subjectIds)); + } + + default Long selectCountByAccountSetIdAndSubjectIds( + Long accountSetId, Collection subjectIds) { + return selectCount(new LambdaQueryWrapperX() + .eq(FmsInitialBalanceDO::getAccountSetId, accountSetId) + .in(FmsInitialBalanceDO::getSubjectId, subjectIds)); + } + + default void updateSubject(Long accountSetId, Long subjectId, FmsInitialBalanceDO updateObj) { + update(updateObj, new LambdaUpdateWrapper() + .eq(FmsInitialBalanceDO::getAccountSetId, accountSetId) + .eq(FmsInitialBalanceDO::getSubjectId, subjectId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsSubjectMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsSubjectMapper.java new file mode 100644 index 0000000..e89e613 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsSubjectMapper.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 会计科目 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsSubjectMapper extends BaseMapperX { + + default List selectListByAccountSetIdAndType(Long accountSetId, Integer type) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsSubjectDO::getAccountSetId, accountSetId) + .eqIfPresent(FmsSubjectDO::getType, type) + .orderByAsc(FmsSubjectDO::getCode) + .orderByAsc(FmsSubjectDO::getId)); + } + + default FmsSubjectDO selectByAccountSetIdAndCode(Long accountSetId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsSubjectDO::getAccountSetId, accountSetId) + .eq(FmsSubjectDO::getCode, code)); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(FmsSubjectDO::getParentId, parentId); + } + + default Long selectCountByParentIds(Collection parentIds) { + return selectCount(new LambdaQueryWrapperX() + .in(FmsSubjectDO::getParentId, parentIds)); + } + + default List selectListByParentIds(Long accountSetId, Collection parentIds) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsSubjectDO::getAccountSetId, accountSetId) + .in(FmsSubjectDO::getParentId, parentIds) + .orderByAsc(FmsSubjectDO::getCode) + .orderByAsc(FmsSubjectDO::getId)); + } + + default FmsSubjectDO selectByIdAndAccountSetId(Long id, Long accountSetId) { + return selectOne(FmsSubjectDO::getId, id, + FmsSubjectDO::getAccountSetId, accountSetId); + } + + default List selectListByIdsAndAccountSetId(Collection ids, Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .in(FmsSubjectDO::getId, ids) + .eq(FmsSubjectDO::getAccountSetId, accountSetId)); + } + + default void updateStatusByIdsAndAccountSetId(Collection ids, Long accountSetId, FmsSubjectDO updateObj) { + update(updateObj, new LambdaUpdateWrapper() + .in(FmsSubjectDO::getId, ids) + .eq(FmsSubjectDO::getAccountSetId, accountSetId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsSubjectTemplateMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsSubjectTemplateMapper.java new file mode 100644 index 0000000..d486078 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsSubjectTemplateMapper.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * FMS 科目模板 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsSubjectTemplateMapper extends BaseMapperX { + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherTemplateCategoryMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherTemplateCategoryMapper.java new file mode 100644 index 0000000..fd5488b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherTemplateCategoryMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 凭证模板分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsVoucherTemplateCategoryMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsVoucherTemplateCategoryDO::getAccountSetId, accountSetId) + .orderByAsc(FmsVoucherTemplateCategoryDO::getId)); + } + + default FmsVoucherTemplateCategoryDO selectByAccountSetIdAndName(Long accountSetId, String name) { + return selectOne(FmsVoucherTemplateCategoryDO::getAccountSetId, accountSetId, + FmsVoucherTemplateCategoryDO::getName, name); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherTemplateMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherTemplateMapper.java new file mode 100644 index 0000000..f9a5a05 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherTemplateMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 凭证模板 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsVoucherTemplateMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsVoucherTemplateDO::getAccountSetId, accountSetId) + .orderByAsc(FmsVoucherTemplateDO::getCategoryId) + .orderByAsc(FmsVoucherTemplateDO::getId)); + } + + default Long selectCountByCategoryId(Long categoryId) { + return selectCount(FmsVoucherTemplateDO::getCategoryId, categoryId); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherWordMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherWordMapper.java new file mode 100644 index 0000000..63c0292 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/config/FmsVoucherWordMapper.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 凭证字 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsVoucherWordMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsVoucherWordDO::getAccountSetId, accountSetId) + .orderByAsc(FmsVoucherWordDO::getSort) + .orderByAsc(FmsVoucherWordDO::getId)); + } + + default FmsVoucherWordDO selectByAccountSetIdAndName(Long accountSetId, String name) { + return selectOne(FmsVoucherWordDO::getAccountSetId, accountSetId, + FmsVoucherWordDO::getName, name); + } + + default FmsVoucherWordDO selectDefaultByAccountSetId(Long accountSetId) { + return selectFirstOne(FmsVoucherWordDO::getAccountSetId, accountSetId, + FmsVoucherWordDO::getDefaultStatus, true); + } + + default FmsVoucherWordDO selectLastByAccountSetId(Long accountSetId) { + return selectLastOne(new LambdaQueryWrapperX() + .eq(FmsVoucherWordDO::getAccountSetId, accountSetId) + .orderByAsc(FmsVoucherWordDO::getSort) + .orderByAsc(FmsVoucherWordDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/ledger/FmsLedgerQueryMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/ledger/FmsLedgerQueryMapper.java new file mode 100644 index 0000000..3319837 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/ledger/FmsLedgerQueryMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.ledger; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerEntryVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * FMS 账簿查询 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsLedgerQueryMapper extends BaseMapperX { + + default List selectEntryListBeforeTime(Long accountSetId, LocalDateTime endTime) { + return selectJoinList(FmsLedgerEntryVO.class, + new MPJLambdaWrapperX() + .selectAs(FmsVoucherEntryDO::getId, FmsLedgerEntryVO::getEntryId) + .selectAs(FmsVoucherEntryDO::getVoucherId, FmsLedgerEntryVO::getVoucherId) + .selectAs(FmsVoucherEntryDO::getSubjectId, FmsLedgerEntryVO::getSubjectId) + .selectAs(FmsVoucherDO::getVoucherTime, FmsLedgerEntryVO::getVoucherTime) + .selectAs(FmsVoucherDO::getVoucherNumber, FmsLedgerEntryVO::getVoucherNumber) + .selectAs(FmsVoucherWordDO::getName, FmsLedgerEntryVO::getVoucherWordName) + .selectAs(FmsVoucherEntryDO::getDigest, FmsLedgerEntryVO::getDigest) + .selectAs(FmsVoucherEntryDO::getDebitAmount, FmsLedgerEntryVO::getDebitAmount) + .selectAs(FmsVoucherEntryDO::getCreditAmount, FmsLedgerEntryVO::getCreditAmount) + .selectAs(FmsVoucherEntryDO::getQuantity, FmsLedgerEntryVO::getQuantity) + .selectAs(FmsVoucherEntryDO::getUnitPrice, FmsLedgerEntryVO::getUnitPrice) + .selectAs(FmsVoucherEntryDO::getSort, FmsLedgerEntryVO::getSort) + .selectAs(FmsVoucherEntryDO::getAssistCombinationId, + FmsLedgerEntryVO::getAssistCombinationId) + .selectAs(FmsVoucherEntryDO::getAuxiliaries, FmsLedgerEntryVO::getAuxiliaries) + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .lt(FmsVoucherDO::getVoucherTime, endTime) + .innerJoin(FmsVoucherDO.class, FmsVoucherDO::getId, + FmsVoucherEntryDO::getVoucherId) + .leftJoin(FmsVoucherWordDO.class, FmsVoucherWordDO::getId, + FmsVoucherDO::getVoucherWordId) + .orderByAsc(FmsVoucherDO::getVoucherTime) + .orderByAsc(FmsVoucherDO::getVoucherNumber) + .orderByAsc(FmsVoucherDO::getId) + .orderByAsc(FmsVoucherEntryDO::getSort) + .orderByAsc(FmsVoucherEntryDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/FmsReportTemplateMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/FmsReportTemplateMapper.java new file mode 100644 index 0000000..08ce250 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/FmsReportTemplateMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 报表模板 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsReportTemplateMapper extends BaseMapperX { + + default List selectListByType(Integer type) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsReportTemplateDO::getType, type) + .orderByAsc(FmsReportTemplateDO::getSort) + .orderByAsc(FmsReportTemplateDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/balance/FmsBalanceSheetConfigMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/balance/FmsBalanceSheetConfigMapper.java new file mode 100644 index 0000000..d229f0e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/balance/FmsBalanceSheetConfigMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.balance; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.balance.FmsBalanceSheetConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 资产负债表配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsBalanceSheetConfigMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsBalanceSheetConfigDO::getAccountSetId, accountSetId) + .orderByAsc(FmsBalanceSheetConfigDO::getSort) + .orderByAsc(FmsBalanceSheetConfigDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/balance/FmsBalanceSheetReportMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/balance/FmsBalanceSheetReportMapper.java new file mode 100644 index 0000000..dccbe20 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/balance/FmsBalanceSheetReportMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.balance; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.balance.FmsBalanceSheetReportDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 资产负债表数据 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsBalanceSheetReportMapper extends BaseMapperX { + + default List selectListByPeriod( + Long accountSetId, Integer fromPeriod, Integer toPeriod, Integer type) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsBalanceSheetReportDO::getAccountSetId, accountSetId) + .eq(FmsBalanceSheetReportDO::getFromPeriod, fromPeriod) + .eq(FmsBalanceSheetReportDO::getToPeriod, toPeriod) + .eq(FmsBalanceSheetReportDO::getType, type) + .orderByAsc(FmsBalanceSheetReportDO::getSort) + .orderByAsc(FmsBalanceSheetReportDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowExtendConfigMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowExtendConfigMapper.java new file mode 100644 index 0000000..b34247e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowExtendConfigMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowExtendConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 现金流量表扩展配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsCashFlowExtendConfigMapper extends BaseMapperX { + + default List selectListByAccountSetIdAndCategory(Long accountSetId, Integer category) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsCashFlowExtendConfigDO::getAccountSetId, accountSetId) + .eqIfPresent(FmsCashFlowExtendConfigDO::getCategory, category) + .orderByAsc(FmsCashFlowExtendConfigDO::getSort) + .orderByAsc(FmsCashFlowExtendConfigDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowExtendDataMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowExtendDataMapper.java new file mode 100644 index 0000000..22ee2f1 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowExtendDataMapper.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowExtendDataDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 现金流量表扩展数据 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsCashFlowExtendDataMapper extends BaseMapperX { + + default List selectListByPeriod(Long accountSetId, Integer fromPeriod, + Integer toPeriod, Integer type, Integer category) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsCashFlowExtendDataDO::getAccountSetId, accountSetId) + .eq(FmsCashFlowExtendDataDO::getFromPeriod, fromPeriod) + .eq(FmsCashFlowExtendDataDO::getToPeriod, toPeriod) + .eq(FmsCashFlowExtendDataDO::getType, type) + .eqIfPresent(FmsCashFlowExtendDataDO::getCategory, category) + .orderByAsc(FmsCashFlowExtendDataDO::getSort) + .orderByAsc(FmsCashFlowExtendDataDO::getId)); + } + + default List selectListByIdsAndAccountSetId(Collection ids, Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .in(FmsCashFlowExtendDataDO::getId, ids) + .eq(FmsCashFlowExtendDataDO::getAccountSetId, accountSetId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowStatementConfigMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowStatementConfigMapper.java new file mode 100644 index 0000000..2815dde --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowStatementConfigMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowStatementConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 现金流量表配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsCashFlowStatementConfigMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsCashFlowStatementConfigDO::getAccountSetId, accountSetId) + .orderByAsc(FmsCashFlowStatementConfigDO::getSort) + .orderByAsc(FmsCashFlowStatementConfigDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowStatementReportMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowStatementReportMapper.java new file mode 100644 index 0000000..60d62df --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/cashflow/FmsCashFlowStatementReportMapper.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowStatementReportDO; +import org.apache.ibatis.annotations.Mapper; + +import java.math.BigDecimal; +import java.util.List; + +/** + * FMS 现金流量表数据 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsCashFlowStatementReportMapper extends BaseMapperX { + + default List selectListByPeriod(Long accountSetId, + Integer fromPeriod, Integer toPeriod, Integer type) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsCashFlowStatementReportDO::getAccountSetId, accountSetId) + .eq(FmsCashFlowStatementReportDO::getFromPeriod, fromPeriod) + .eq(FmsCashFlowStatementReportDO::getToPeriod, toPeriod) + .eq(FmsCashFlowStatementReportDO::getType, type) + .orderByAsc(FmsCashFlowStatementReportDO::getSort) + .orderByAsc(FmsCashFlowStatementReportDO::getId)); + } + + default List selectListByIdsAndPeriod(List ids, + Long accountSetId, Integer fromPeriod, Integer toPeriod, Integer type) { + return selectList(new LambdaQueryWrapperX() + .in(FmsCashFlowStatementReportDO::getId, ids) + .eq(FmsCashFlowStatementReportDO::getAccountSetId, accountSetId) + .eq(FmsCashFlowStatementReportDO::getFromPeriod, fromPeriod) + .eq(FmsCashFlowStatementReportDO::getToPeriod, toPeriod) + .eq(FmsCashFlowStatementReportDO::getType, type)); + } + + default void clearAdjustedAmounts(Long accountSetId, Integer fromPeriod, Integer toPeriod, Integer type) { + update(new FmsCashFlowStatementReportDO(), + new LambdaUpdateWrapper() + .eq(FmsCashFlowStatementReportDO::getAccountSetId, accountSetId) + .eq(FmsCashFlowStatementReportDO::getFromPeriod, fromPeriod) + .eq(FmsCashFlowStatementReportDO::getToPeriod, toPeriod) + .eq(FmsCashFlowStatementReportDO::getType, type) + .set(FmsCashFlowStatementReportDO::getCurrentAmount, BigDecimal.ZERO) + .set(FmsCashFlowStatementReportDO::getYearAmount, BigDecimal.ZERO)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/income/FmsIncomeStatementConfigMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/income/FmsIncomeStatementConfigMapper.java new file mode 100644 index 0000000..c7ef633 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/income/FmsIncomeStatementConfigMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.income; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.income.FmsIncomeStatementConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 利润表配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsIncomeStatementConfigMapper extends BaseMapperX { + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsIncomeStatementConfigDO::getAccountSetId, accountSetId) + .orderByAsc(FmsIncomeStatementConfigDO::getSort) + .orderByAsc(FmsIncomeStatementConfigDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/income/FmsIncomeStatementReportMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/income/FmsIncomeStatementReportMapper.java new file mode 100644 index 0000000..f19b046 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/report/income/FmsIncomeStatementReportMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.report.income; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.income.FmsIncomeStatementReportDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * FMS 利润表数据 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsIncomeStatementReportMapper extends BaseMapperX { + + default List selectListByPeriod(Long accountSetId, + Integer fromPeriod, Integer toPeriod, Integer type) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsIncomeStatementReportDO::getAccountSetId, accountSetId) + .eq(FmsIncomeStatementReportDO::getFromPeriod, fromPeriod) + .eq(FmsIncomeStatementReportDO::getToPeriod, toPeriod) + .eq(FmsIncomeStatementReportDO::getType, type) + .orderByAsc(FmsIncomeStatementReportDO::getSort) + .orderByAsc(FmsIncomeStatementReportDO::getId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/voucher/FmsVoucherEntryMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/voucher/FmsVoucherEntryMapper.java new file mode 100644 index 0000000..83af3e8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/voucher/FmsVoucherEntryMapper.java @@ -0,0 +1,127 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.voucher; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSubjectAmountVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +/** + * FMS 凭证分录 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsVoucherEntryMapper extends BaseMapperX { + + default List selectListByVoucherId(Long voucherId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsVoucherEntryDO::getVoucherId, voucherId) + .orderByAsc(FmsVoucherEntryDO::getSort) + .orderByAsc(FmsVoucherEntryDO::getId)); + } + + default List selectListByVoucherIds(Collection voucherIds) { + return selectList(new LambdaQueryWrapperX() + .in(FmsVoucherEntryDO::getVoucherId, voucherIds) + .orderByAsc(FmsVoucherEntryDO::getVoucherId) + .orderByAsc(FmsVoucherEntryDO::getSort) + .orderByAsc(FmsVoucherEntryDO::getId)); + } + + default List selectListByAccountSetId(Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .orderByAsc(FmsVoucherEntryDO::getId)); + } + + default List selectListByAccountSetIdAndSubjectId(Long accountSetId, Long subjectId) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .eq(FmsVoucherEntryDO::getSubjectId, subjectId) + .orderByAsc(FmsVoucherEntryDO::getId)); + } + + default List selectVoucherIdsByCondition(Long accountSetId, Collection subjectIds, String digest, + BigDecimal minAmount, BigDecimal maxAmount) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .inIfPresent(FmsVoucherEntryDO::getSubjectId, subjectIds) + .likeIfPresent(FmsVoucherEntryDO::getDigest, digest); + query.apply(minAmount != null, "(debit_amount + credit_amount) >= {0}", minAmount) + .apply(maxAmount != null, "(debit_amount + credit_amount) <= {0}", maxAmount); + query.select(FmsVoucherEntryDO::getVoucherId); + return selectList(query); + } + + default void deleteByVoucherIds(Collection voucherIds) { + delete(new LambdaQueryWrapperX() + .in(FmsVoucherEntryDO::getVoucherId, voucherIds)); + } + + default Long selectCountByAccountSetIdAndSubjectIds(Long accountSetId, Collection subjectIds) { + return selectCount(new LambdaQueryWrapperX() + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .in(FmsVoucherEntryDO::getSubjectId, subjectIds)); + } + + default Long selectQuantityCountByAccountSetIdAndSubjectIds(Long accountSetId, Collection subjectIds) { + return selectCount(new LambdaQueryWrapperX() + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .in(FmsVoucherEntryDO::getSubjectId, subjectIds) + .isNotNull(FmsVoucherEntryDO::getQuantity) + .ne(FmsVoucherEntryDO::getQuantity, BigDecimal.ZERO)); + } + + default void updateSubject(Long accountSetId, Long subjectId, FmsVoucherEntryDO updateObj) { + update(updateObj, new LambdaUpdateWrapper() + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .eq(FmsVoucherEntryDO::getSubjectId, subjectId)); + } + + default List selectSubjectAmountList(Long accountSetId, + LocalDateTime beginTime, LocalDateTime endTime, + Long voucherWordId, Integer minVoucherNumber, Integer maxVoucherNumber) { + return selectJoinList(FmsVoucherSubjectAmountVO.class, + new MPJLambdaWrapperX() + .selectAs(FmsVoucherEntryDO::getSubjectId, + FmsVoucherSubjectAmountVO::getSubjectId) + .selectSum(FmsVoucherEntryDO::getDebitAmount, + FmsVoucherSubjectAmountVO::getDebitAmount) + .selectSum(FmsVoucherEntryDO::getCreditAmount, + FmsVoucherSubjectAmountVO::getCreditAmount) + .eq(FmsVoucherEntryDO::getAccountSetId, accountSetId) + .eqIfPresent(FmsVoucherDO::getVoucherWordId, voucherWordId) + .geIfPresent(FmsVoucherDO::getVoucherNumber, minVoucherNumber) + .leIfPresent(FmsVoucherDO::getVoucherNumber, maxVoucherNumber) + .ge(FmsVoucherDO::getVoucherTime, beginTime) + .lt(FmsVoucherDO::getVoucherTime, endTime) + .innerJoin(FmsVoucherDO.class, FmsVoucherDO::getId, + FmsVoucherEntryDO::getVoucherId) + .groupBy(FmsVoucherEntryDO::getSubjectId) + .orderByAsc(FmsVoucherEntryDO::getSubjectId)); + } + + default List selectSubjectAmountListByVoucherIds(Collection voucherIds) { + return selectJoinList(FmsVoucherSubjectAmountVO.class, + new MPJLambdaWrapperX() + .selectAs(FmsVoucherEntryDO::getSubjectId, + FmsVoucherSubjectAmountVO::getSubjectId) + .selectSum(FmsVoucherEntryDO::getDebitAmount, + FmsVoucherSubjectAmountVO::getDebitAmount) + .selectSum(FmsVoucherEntryDO::getCreditAmount, + FmsVoucherSubjectAmountVO::getCreditAmount) + .in(FmsVoucherEntryDO::getVoucherId, voucherIds) + .groupBy(FmsVoucherEntryDO::getSubjectId) + .orderByAsc(FmsVoucherEntryDO::getSubjectId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/voucher/FmsVoucherMapper.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/voucher/FmsVoucherMapper.java new file mode 100644 index 0000000..ea43d9b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/dal/mysql/voucher/FmsVoucherMapper.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.fms.dal.mysql.voucher; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherPageReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherTidyTypeEnum; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +/** + * FMS 凭证 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FmsVoucherMapper extends BaseMapperX { + + default void updateReviewStatusById(Long id, Integer status, Long reviewerUserId) { + update(new LambdaUpdateWrapper() + .eq(FmsVoucherDO::getId, id) + .set(FmsVoucherDO::getStatus, status) + .set(FmsVoucherDO::getReviewerUserId, reviewerUserId)); + } + + default PageResult selectPage(FmsVoucherPageReqVO reqVO, Collection voucherIds) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eq(FmsVoucherDO::getAccountSetId, reqVO.getAccountSetId()) + .inIfPresent(FmsVoucherDO::getId, reqVO.getIds()) + .betweenIfPresent(FmsVoucherDO::getVoucherTime, reqVO.getVoucherTime()) + .eqIfPresent(FmsVoucherDO::getVoucherWordId, reqVO.getVoucherWordId()) + .eqIfPresent(FmsVoucherDO::getVoucherNumber, reqVO.getVoucherNumber()) + .eqIfPresent(FmsVoucherDO::getStatus, reqVO.getStatus()) + .eqIfPresent(FmsVoucherDO::getCreator, reqVO.getCreatorUserId() == null ? null : reqVO.getCreatorUserId().toString()) + .inIfPresent(FmsVoucherDO::getId, voucherIds); + return selectPage(reqVO, query.orderByAsc(FmsVoucherDO::getVoucherTime) + .orderByAsc(FmsVoucherDO::getVoucherWordId) + .orderByAsc(FmsVoucherDO::getVoucherNumber) + .orderByAsc(FmsVoucherDO::getId)); + } + + default FmsVoucherDO selectByAccountSetIdAndVoucherWordIdAndVoucherNumberAndVoucherTimeBetween( + Long accountSetId, Long voucherWordId, Integer voucherNumber, + LocalDateTime beginTime, LocalDateTime endTime) { + return selectOne(new LambdaQueryWrapperX() + .eq(FmsVoucherDO::getAccountSetId, accountSetId) + .eq(FmsVoucherDO::getVoucherWordId, voucherWordId) + .eq(FmsVoucherDO::getVoucherNumber, voucherNumber) + .ge(FmsVoucherDO::getVoucherTime, beginTime) + .lt(FmsVoucherDO::getVoucherTime, endTime)); + } + + default FmsVoucherDO selectLastByPeriod(Long accountSetId, Long voucherWordId, LocalDateTime monthBeginTime, + LocalDateTime nextMonthBeginTime) { + return selectLastOne(new LambdaQueryWrapperX() + .eq(FmsVoucherDO::getAccountSetId, accountSetId) + .eq(FmsVoucherDO::getVoucherWordId, voucherWordId) + .ge(FmsVoucherDO::getVoucherTime, monthBeginTime) + .lt(FmsVoucherDO::getVoucherTime, nextMonthBeginTime) + .orderByAsc(FmsVoucherDO::getVoucherNumber) + .orderByAsc(FmsVoucherDO::getId)); + } + + default List selectListByIdsAndAccountSetId(Collection ids, Long accountSetId) { + return selectList(new LambdaQueryWrapperX() + .in(FmsVoucherDO::getId, ids) + .eq(FmsVoucherDO::getAccountSetId, accountSetId)); + } + + default List selectListByAccountSetIdAndVoucherTimeBetween(Long accountSetId, + LocalDateTime beginTime, LocalDateTime endTime) { + return selectList(new LambdaQueryWrapperX() + .eq(FmsVoucherDO::getAccountSetId, accountSetId) + .ge(FmsVoucherDO::getVoucherTime, beginTime) + .lt(FmsVoucherDO::getVoucherTime, endTime) + .orderByAsc(FmsVoucherDO::getVoucherWordId) + .orderByAsc(FmsVoucherDO::getVoucherNumber) + .orderByAsc(FmsVoucherDO::getId)); + } + + /** + * 查询指定会计期间和起始号码后的待整理凭证:按凭证字分组,并根据整理方式按日期或凭证号优先排序 + */ + default List selectListForTidy(Long accountSetId, LocalDateTime monthBeginTime, + LocalDateTime nextMonthBeginTime, Long voucherWordId, Integer startNumber, Integer type) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX<>(); + query.eq(FmsVoucherDO::getAccountSetId, accountSetId); + query.eqIfPresent(FmsVoucherDO::getVoucherWordId, voucherWordId); + query.ge(FmsVoucherDO::getVoucherTime, monthBeginTime) + .lt(FmsVoucherDO::getVoucherTime, nextMonthBeginTime) + .ge(FmsVoucherDO::getVoucherNumber, startNumber) + .orderByAsc(FmsVoucherDO::getVoucherWordId); + if (FmsVoucherTidyTypeEnum.REORDER_BY_TIME.getType().equals(type)) { + query.orderByAsc(FmsVoucherDO::getVoucherTime) + .orderByAsc(FmsVoucherDO::getVoucherNumber); + } else { + query.orderByAsc(FmsVoucherDO::getVoucherNumber) + .orderByAsc(FmsVoucherDO::getVoucherTime); + } + return selectList(query.orderByAsc(FmsVoucherDO::getId)); + } + + /** + * 查询指定会计期间内目标号码到原凭证号码之间的凭证:按凭证号、日期和编号升序,供移动时稳定顺延中间凭证 + */ + default List selectListForMove(Long accountSetId, Long voucherWordId, + LocalDateTime monthBeginTime, LocalDateTime nextMonthBeginTime, + Integer targetNumber, Integer sourceNumber) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX<>(); + query.eq(FmsVoucherDO::getAccountSetId, accountSetId); + query.eq(FmsVoucherDO::getVoucherWordId, voucherWordId); + query.ge(FmsVoucherDO::getVoucherTime, monthBeginTime) + .lt(FmsVoucherDO::getVoucherTime, nextMonthBeginTime) + .between(FmsVoucherDO::getVoucherNumber, targetNumber, sourceNumber) + .orderByAsc(FmsVoucherDO::getVoucherNumber) + .orderByAsc(FmsVoucherDO::getVoucherTime) + .orderByAsc(FmsVoucherDO::getId); + return selectList(query); + } + + default Long selectCountByAccountSetIdAndVoucherWordId(Long accountSetId, Long voucherWordId) { + return selectCount(new LambdaQueryWrapperX() + .eq(FmsVoucherDO::getAccountSetId, accountSetId) + .eq(FmsVoucherDO::getVoucherWordId, voucherWordId)); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/DictTypeConstants.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/DictTypeConstants.java new file mode 100644 index 0000000..7001b42 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/DictTypeConstants.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.fms.enums; + +/** + * FMS 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String ACCOUNT_USER_LEVEL = "fms_account_user_level"; // 账套用户权限级别 + String SUBJECT_CATEGORY = "fms_subject_category"; // 科目类别 + String SUBJECT_TYPE = "fms_subject_type"; // 科目类型 + String AUXILIARY_TYPE = "fms_auxiliary_type"; // 辅助核算类别 + String DEBIT_CREDIT_DIRECTION = "fms_debit_credit_direction"; // 借贷方向 + String VOUCHER_STATUS = "fms_voucher_status"; // 凭证状态 + String VOUCHER_TIDY_TYPE = "fms_voucher_tidy_type"; // 凭证整理方式 + String FINANCE_INDICATOR_TYPE = "fms_finance_indicator_type"; // 财务指标取数报表类型 + String FORMULA_RULE = "fms_formula_rule"; // 报表公式取数规则 + String REPORT_TYPE = "fms_report_type"; // 财务报表类型 + String REPORT_PERIOD_TYPE = "fms_report_period_type"; // 财务报表期间类型 + String LEDGER_BALANCE_MODE = "fms_ledger_balance_mode"; // 账簿余额方向模式 + String ACCOUNTING_STANDARD = "fms_accounting_standard"; // 会计制度 + String CLOSING_TYPE = "fms_closing_type"; // 结账方案类型 + String CLOSING_TIME_TYPE = "fms_closing_time_type"; // 结账取数期间 + String CLOSING_VOUCHER_TYPE = "fms_closing_voucher_type"; // 结账凭证类型 + String CLOSING_TEMPLATE_CATEGORY = "fms_closing_template_category"; // 结账模板分类 +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/ErrorCodeConstants.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..27d1ae6 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/ErrorCodeConstants.java @@ -0,0 +1,189 @@ +package cn.iocoder.yudao.module.fms.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * FMS 错误码常量 + * + * @author 芋道源码 + */ +public interface ErrorCodeConstants { + + // ========== 账套 1-052-100-000 ========== + ErrorCode ACCOUNT_SET_NOT_EXISTS = new ErrorCode(1_052_100_000, "账套不存在"); + ErrorCode ACCOUNT_SET_CODE_DUPLICATE = new ErrorCode(1_052_100_001, "账套编码已存在"); + ErrorCode ACCOUNT_SET_ACCESS_DENIED = new ErrorCode(1_052_100_002, "无权访问该账套"); + ErrorCode ACCOUNT_SET_ALREADY_INITIALIZED = new ErrorCode(1_052_100_003, "账套已完成初始化"); + ErrorCode ACCOUNT_SET_NOT_INITIALIZED = new ErrorCode(1_052_100_005, "账套尚未完成初始化"); + ErrorCode ACCOUNT_SET_SETTINGS_RULE_INVALID = new ErrorCode(1_052_100_006, "科目编码规则不正确"); + ErrorCode ACCOUNT_SET_SETTINGS_RULE_SHRINK = new ErrorCode(1_052_100_007, "科目级次和编码长度不能调小"); + ErrorCode FINANCE_PARAMETER_NOT_EXISTS = new ErrorCode(1_052_100_008, "财务参数不存在"); + + // ========== 科目 1-052-101-000 ========== + ErrorCode SUBJECT_NOT_EXISTS = new ErrorCode(1_052_101_000, "科目不存在"); + ErrorCode SUBJECT_CODE_DUPLICATE = new ErrorCode(1_052_101_001, "科目编码已存在"); + ErrorCode SUBJECT_CATEGORY_INVALID = new ErrorCode(1_052_101_002, "科目类别与科目类型不匹配"); + ErrorCode SUBJECT_PARENT_INVALID = new ErrorCode(1_052_101_003, + "上级科目不属于当前账套,或科目类型、类别不一致"); + ErrorCode SUBJECT_CODE_RULE_INVALID = new ErrorCode(1_052_101_004, "科目编码不符合科目级次规则"); + ErrorCode SUBJECT_HAS_CHILDREN = new ErrorCode(1_052_101_005, "科目下有子级科目,不能删除或修改编码"); + ErrorCode SUBJECT_QUANTITY_UNIT_REQUIRED = new ErrorCode(1_052_101_006, "启用数量核算时数量单位不能为空"); + ErrorCode SUBJECT_CURRENCY_INVALID = new ErrorCode(1_052_101_007, "外币核算不能选择本位币"); + ErrorCode SUBJECT_VOUCHER_ENTRY_IN_USE = new ErrorCode(1_052_101_008, + "科目已被 {} 条凭证分录使用,不能删除"); + ErrorCode SUBJECT_VOUCHER_TEMPLATE_IN_USE = new ErrorCode(1_052_101_009, + "科目已被 {} 个凭证模板使用,不能删除"); + ErrorCode SUBJECT_INITIAL_BALANCE_IN_USE = new ErrorCode(1_052_101_010, + "科目已被 {} 条初始余额使用,不能删除"); + ErrorCode SUBJECT_AUXILIARY_COMBINATION_IN_USE = new ErrorCode(1_052_101_011, + "科目已被 {} 个辅助核算组合使用,不能删除"); + ErrorCode SUBJECT_CLOSING_SCHEME_IN_USE = new ErrorCode(1_052_101_012, + "科目已被 {} 个结账方案使用,不能删除"); + ErrorCode SUBJECT_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_052_101_013, "导入科目数据不能为空"); + ErrorCode SUBJECT_USED_BALANCE_DIRECTION_IMMUTABLE = new ErrorCode(1_052_101_014, + "科目已有业务数据,不能修改余额方向"); + ErrorCode SUBJECT_USED_AUXILIARY_IMMUTABLE = new ErrorCode(1_052_101_015, + "科目已有业务数据,不能直接修改辅助核算"); + ErrorCode SUBJECT_NON_LEAF_CONFIG_IMMUTABLE = new ErrorCode(1_052_101_016, + "非末级科目不能修改科目类型、类别或辅助核算"); + ErrorCode SUBJECT_QUANTITY_ACCOUNTING_IN_USE = new ErrorCode(1_052_101_017, + "科目已有数量数据,不能关闭数量核算"); + ErrorCode SUBJECT_PARENT_IN_USE = new ErrorCode(1_052_101_018, + "上级科目已有业务数据,新增下级前需要迁移历史数据"); + ErrorCode SUBJECT_PARENT_CONFIG_INCOMPATIBLE = new ErrorCode(1_052_101_019, + "迁移历史数据时,下级科目的余额方向和核算配置必须与上级科目一致"); + ErrorCode SUBJECT_AUXILIARY_MIGRATION_INVALID = new ErrorCode(1_052_101_020, + "科目已有业务数据,新增辅助核算时必须为每个类别选择有效的迁移项目"); + ErrorCode SUBJECT_CLOSING_TEMPLATE_IN_USE = new ErrorCode(1_052_101_021, + "科目已被 {} 个结账模板使用,不能删除"); + + // ========== 辅助核算 1-052-102-000 ========== + ErrorCode AUXILIARY_TYPE_NOT_EXISTS = new ErrorCode(1_052_102_000, "辅助核算类别不存在"); + ErrorCode AUXILIARY_TYPE_NAME_DUPLICATE = new ErrorCode(1_052_102_001, "辅助核算类别名称已存在"); + ErrorCode AUXILIARY_TYPE_SYSTEM_NOT_EDITABLE = new ErrorCode(1_052_102_002, "系统预置辅助核算类别不能编辑或删除"); + ErrorCode AUXILIARY_TYPE_HAS_ITEM = new ErrorCode(1_052_102_003, + "辅助核算类别下有 {} 个项目,不能删除"); + ErrorCode AUXILIARY_ITEM_NOT_EXISTS = new ErrorCode(1_052_102_004, "辅助核算项目不存在"); + ErrorCode AUXILIARY_ITEM_CODE_DUPLICATE = new ErrorCode(1_052_102_005, "辅助核算项目编码已存在"); + ErrorCode AUXILIARY_TYPE_SUBJECT_IN_USE = new ErrorCode(1_052_102_006, + "辅助核算类别已被 {} 个科目使用,不能删除"); + ErrorCode AUXILIARY_ITEM_VOUCHER_IN_USE = new ErrorCode(1_052_102_007, + "辅助核算项目已被 {} 条凭证分录使用,不能删除"); + ErrorCode AUXILIARY_ITEM_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_052_102_008, + "导入辅助核算项目数据不能为空"); + ErrorCode AUXILIARY_ITEM_VOUCHER_TEMPLATE_IN_USE = new ErrorCode(1_052_102_009, + "辅助核算项目已被 {} 个凭证模板使用,不能删除"); + ErrorCode AUXILIARY_ITEM_INITIAL_BALANCE_IN_USE = new ErrorCode(1_052_102_010, + "辅助核算项目已被 {} 条初始余额使用,不能删除"); + ErrorCode AUXILIARY_TYPE_VOUCHER_IN_USE = new ErrorCode(1_052_102_012, + "辅助核算类别已被 {} 条凭证分录使用,不能删除"); + ErrorCode AUXILIARY_TYPE_VOUCHER_TEMPLATE_IN_USE = new ErrorCode(1_052_102_013, + "辅助核算类别已被 {} 个凭证模板使用,不能删除"); + ErrorCode AUXILIARY_TYPE_INITIAL_BALANCE_IN_USE = new ErrorCode(1_052_102_014, + "辅助核算类别已被 {} 条初始余额使用,不能删除"); + + // ========== 凭证 1-052-103-000 ========== + ErrorCode VOUCHER_NOT_EXISTS = new ErrorCode(1_052_103_000, "凭证不存在"); + ErrorCode VOUCHER_WORD_NOT_EXISTS = new ErrorCode(1_052_103_001, "凭证字不存在"); + ErrorCode VOUCHER_NUMBER_DUPLICATE = new ErrorCode(1_052_103_002, "该期间的凭证号已存在"); + ErrorCode VOUCHER_ENTRY_REQUIRED = new ErrorCode(1_052_103_003, "凭证至少需要两条有效分录"); + ErrorCode VOUCHER_ENTRY_AMOUNT_INVALID = new ErrorCode(1_052_103_004, "凭证分录只能填写借方或贷方金额"); + ErrorCode VOUCHER_AMOUNT_UNBALANCED = new ErrorCode(1_052_103_005, "凭证借贷金额不平衡"); + ErrorCode VOUCHER_APPROVED_NOT_EDITABLE = new ErrorCode(1_052_103_006, "已审核凭证不能编辑或删除"); + ErrorCode VOUCHER_STATUS_INVALID = new ErrorCode(1_052_103_007, "凭证状态流转不正确"); + ErrorCode VOUCHER_SUBJECT_DISABLED = new ErrorCode(1_052_103_008, "凭证分录使用了已停用科目"); + ErrorCode VOUCHER_SUBJECT_HAS_CHILDREN = new ErrorCode(1_052_103_009, "凭证分录不能使用非末级科目"); + ErrorCode VOUCHER_AUXILIARY_REQUIRED = new ErrorCode(1_052_103_010, "凭证分录的辅助核算项目不完整"); + ErrorCode VOUCHER_QUANTITY_INVALID = new ErrorCode(1_052_103_011, "凭证分录的数量核算信息不正确"); + ErrorCode VOUCHER_WORD_NAME_DUPLICATE = new ErrorCode(1_052_103_012, "凭证字已存在"); + ErrorCode VOUCHER_WORD_DEFAULT_REQUIRED = new ErrorCode(1_052_103_013, "请确保至少有一个默认凭证字"); + ErrorCode VOUCHER_WORD_DEFAULT_NOT_DELETABLE = new ErrorCode(1_052_103_014, "默认凭证字不允许删除"); + ErrorCode VOUCHER_WORD_IN_USE_NOT_EDITABLE = new ErrorCode(1_052_103_015, "使用中的凭证字不能编辑"); + ErrorCode VOUCHER_WORD_IN_USE_NOT_DELETABLE = new ErrorCode(1_052_103_016, + "该凭证字已被 {} 张凭证使用,不能删除"); + ErrorCode VOUCHER_TEMPLATE_CATEGORY_NOT_EXISTS = new ErrorCode(1_052_103_017, "凭证模板分类不存在"); + ErrorCode VOUCHER_TEMPLATE_CATEGORY_NAME_DUPLICATE = new ErrorCode(1_052_103_018, "凭证模板分类已存在"); + ErrorCode VOUCHER_TEMPLATE_CATEGORY_IN_USE = new ErrorCode(1_052_103_019, "分类下存在凭证模板,不能删除"); + ErrorCode VOUCHER_TEMPLATE_NOT_EXISTS = new ErrorCode(1_052_103_020, "凭证模板不存在"); + ErrorCode DIGEST_NOT_EXISTS = new ErrorCode(1_052_103_021, "常用摘要不存在"); + ErrorCode VOUCHER_STATISTICS_RANGE_INVALID = new ErrorCode(1_052_103_022, "凭证汇总查询范围不正确"); + ErrorCode VOUCHER_MOVE_RANGE_INVALID = new ErrorCode(1_052_103_023, "移动到的凭证号必须小于原凭证号"); + ErrorCode VOUCHER_MOVE_SOURCE_NOT_EXISTS = new ErrorCode(1_052_103_024, "要移动的凭证不存在"); + ErrorCode VOUCHER_IMPORT_FILE_EMPTY = new ErrorCode(1_052_103_025, "请选择要导入的凭证文件"); + ErrorCode VOUCHER_IMPORT_FILE_TOO_LARGE = new ErrorCode(1_052_103_026, "导入文件不能超过 2MB"); + ErrorCode VOUCHER_IMPORT_TEMPLATE_INVALID = new ErrorCode(1_052_103_027, "凭证导入模板格式不正确"); + ErrorCode VOUCHER_CLOSING_GENERATED_NOT_EDITABLE = new ErrorCode(1_052_103_029, "结账生成凭证不能编辑、审核或删除"); + ErrorCode VOUCHER_WORD_IN_USE_BY_CLOSING_SCHEME_NOT_DELETABLE = new ErrorCode(1_052_103_030, + "该凭证字已被 {} 个结账方案使用,不能删除"); + + // ========== 账簿 1-052-104-000 ========== + ErrorCode LEDGER_PERIOD_INVALID = new ErrorCode(1_052_104_000, "开始会计期间不能晚于结束会计期间"); + ErrorCode INITIAL_BALANCE_SUBJECT_NOT_LEAF = new ErrorCode(1_052_104_001, "初始余额只能录入末级科目"); + ErrorCode INITIAL_BALANCE_AUXILIARY_INVALID = new ErrorCode(1_052_104_002, "辅助核算项目与科目配置不一致"); + ErrorCode INITIAL_BALANCE_PROFIT_LOSS_YEAR_OPENING_INVALID = new ErrorCode(1_052_104_003, + "损益类科目的年初余额必须为 0"); + ErrorCode INITIAL_BALANCE_IMPORT_TEMPLATE_INVALID = new ErrorCode(1_052_104_006, + "初始余额导入模板格式不正确"); + ErrorCode INITIAL_BALANCE_IMPORT_ROW_INVALID = new ErrorCode(1_052_104_008, + "第 {} 行导入失败:{}"); + ErrorCode INITIAL_BALANCE_PERIOD_CLOSED = new ErrorCode(1_052_104_009, + "账套已结账,不能修改初始余额"); + ErrorCode LEDGER_PERIOD_BEFORE_ACCOUNT_START = new ErrorCode(1_052_104_010, + "开始会计期间不能早于账套启用期间"); + + // ========== 结账 1-052-105-000 ========== + ErrorCode CLOSING_PERIOD_CLOSED = new ErrorCode(1_052_105_000, "该会计期间已结账"); + ErrorCode CLOSING_PERIOD_NOT_CLOSED = new ErrorCode(1_052_105_001, "该会计期间尚未结账"); + ErrorCode CLOSING_NOT_LATEST_PERIOD = new ErrorCode(1_052_105_002, "只能反结账最近的已结账期间"); + ErrorCode CLOSING_VOUCHER_PENDING_REVIEW = new ErrorCode(1_052_105_003, "存在未审核凭证,不能结账"); + ErrorCode CLOSING_PROFIT_LOSS_NOT_TRANSFERRED = new ErrorCode(1_052_105_004, "损益类科目尚有余额,请先结转损益"); + ErrorCode CLOSING_BALANCE_SHEET_UNBALANCED = new ErrorCode(1_052_105_005, "资产负债表不平衡,不能结账"); + ErrorCode CLOSING_PROFIT_SUBJECT_INVALID = new ErrorCode(1_052_105_006, "本年利润科目不正确"); + ErrorCode CLOSING_NO_PROFIT_LOSS_BALANCE = new ErrorCode(1_052_105_007, "当前期间无需结转损益"); + ErrorCode CLOSING_SCHEME_NOT_EXISTS = new ErrorCode(1_052_105_008, "结账方案不存在"); + ErrorCode CLOSING_SCHEME_SUBJECT_INVALID = new ErrorCode(1_052_105_009, "结账方案科目不正确"); + ErrorCode CLOSING_SCHEME_RATIO_INVALID = new ErrorCode(1_052_105_010, "结账方案借贷金额比例必须分别等于 100%"); + ErrorCode CLOSING_SCHEME_NO_BALANCE = new ErrorCode(1_052_105_011, "当前期间无需生成该结转凭证"); + ErrorCode CLOSING_SCHEME_IN_USE = new ErrorCode(1_052_105_012, "结账方案已生成凭证,不能删除"); + ErrorCode CLOSING_NOT_CURRENT_PERIOD = new ErrorCode(1_052_105_013, "只能操作当前会计期间"); + ErrorCode CLOSING_PROFIT_LOSS_SETTINGS_INVALID = new ErrorCode(1_052_105_014, "结转损益科目设置不正确"); + ErrorCode CLOSING_PROFIT_LOSS_SETTINGS_NOT_EXISTS = new ErrorCode(1_052_105_015, "请先完成结转损益参数设置"); + ErrorCode CLOSING_SPECIAL_SETTINGS_INVALID = new ErrorCode(1_052_105_016, "专用结转设置不正确"); + ErrorCode CLOSING_SPECIAL_SUBJECT_NOT_EXISTS = new ErrorCode(1_052_105_017, "专用结转所需的预置科目不存在"); + ErrorCode CLOSING_INITIAL_BALANCE_UNBALANCED = new ErrorCode(1_052_105_018, "初始余额试算不平衡,不能结账"); + ErrorCode CLOSING_VOUCHER_NUMBER_DISCONTINUOUS = new ErrorCode(1_052_105_019, + "当前期间存在断号凭证,请先整理凭证号"); + ErrorCode CLOSING_PROFIT_LOSS_VOUCHER_MISSING = new ErrorCode(1_052_105_020, + "当前期间存在损益发生额,请先生成结转损益凭证"); + ErrorCode CLOSING_INCOME_STATEMENT_UNBALANCED = new ErrorCode(1_052_105_021, + "利润表勾稽不平衡,不能结账"); + ErrorCode CLOSING_REPORT_SUBJECT_UNMAPPED = new ErrorCode(1_052_105_022, + "存在未纳入报表公式的科目,不能结账"); + ErrorCode CLOSING_TEMPLATE_NOT_EXISTS = new ErrorCode(1_052_105_023, "结账模板不存在"); + ErrorCode CLOSING_TEMPLATE_PRESET_INVALID = new ErrorCode(1_052_105_024, "结账模板预置数据不正确"); + ErrorCode CLOSING_TEMPLATE_PRESET_SUBJECT_NOT_EXISTS = new ErrorCode(1_052_105_025, + "结账模板【{}】所需科目【{}】不存在"); + ErrorCode CLOSING_SCHEME_PRESET_SUBJECT_NOT_EXISTS = new ErrorCode(1_052_105_026, + "结账方案【{}】所需科目【{}】不存在"); + ErrorCode CLOSING_SCHEME_NOT_PERIOD_END = new ErrorCode(1_052_105_027, + "该结账方案未启用期末结转"); + + // ========== 报表 1-052-106-000 ========== + ErrorCode REPORT_CONFIG_NOT_EXISTS = new ErrorCode(1_052_106_000, "报表项目不存在"); + ErrorCode REPORT_CONFIG_NOT_EDITABLE = new ErrorCode(1_052_106_001, "该报表项目不允许编辑公式"); + ErrorCode REPORT_FORMULA_SUBJECT_DUPLICATE = new ErrorCode(1_052_106_002, "报表公式科目不能重复"); + ErrorCode REPORT_FORMULA_RULE_INVALID = new ErrorCode(1_052_106_003, "报表公式取数规则不正确"); + ErrorCode REPORT_FORMULA_OPERATOR_INVALID = new ErrorCode(1_052_106_004, "报表公式运算符不正确"); + ErrorCode REPORT_ADJUSTMENT_NOT_EXISTS = new ErrorCode(1_052_106_005, "现金流量辅助数据不存在"); + ErrorCode REPORT_AMOUNT_NOT_ADJUSTABLE = new ErrorCode(1_052_106_006, "该现金流量表项目不允许调整金额"); + + // ========== 币种 1-052-107-000 ========== + ErrorCode CURRENCY_NOT_EXISTS = new ErrorCode(1_052_107_000, "币别不存在"); + ErrorCode CURRENCY_CODE_DUPLICATE = new ErrorCode(1_052_107_001, "币别编码已存在"); + ErrorCode CURRENCY_STANDARD_NOT_DELETABLE = new ErrorCode(1_052_107_002, "本位币不允许删除"); + ErrorCode CURRENCY_IN_USE = new ErrorCode(1_052_107_003, "币别已被科目使用,不能删除"); + + // ========== 首页 1-052-108-000 ========== + ErrorCode HOME_METRIC_INVALID = new ErrorCode(1_052_108_001, "首页财务指标不正确"); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/LogRecordConstants.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/LogRecordConstants.java new file mode 100644 index 0000000..6c67bd0 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/LogRecordConstants.java @@ -0,0 +1,185 @@ +package cn.iocoder.yudao.module.fms.enums; + +/** + * FMS 操作日志常量 + * + * @author 芋道源码 + */ +public interface LogRecordConstants { + + // ======================= FMS_ACCOUNT_SET 账套 ======================= + + String FMS_ACCOUNT_SET_TYPE = "FMS 账套"; + String FMS_ACCOUNT_SET_CREATE_SUB_TYPE = "创建账套"; + String FMS_ACCOUNT_SET_CREATE_SUCCESS = "创建了账套【{{#createReqVO.companyName}}】"; + String FMS_ACCOUNT_SET_UPDATE_SUB_TYPE = "更新账套"; + String FMS_ACCOUNT_SET_UPDATE_SUCCESS = + "更新了账套【{{#updateReqVO.companyName}}】: {_DIFF{#updateReqVO}}"; + String FMS_ACCOUNT_SET_INITIALIZE_SUB_TYPE = "初始化账套"; + String FMS_ACCOUNT_SET_INITIALIZE_SUCCESS = "初始化了账套【{{#accountSet.companyName}}】"; + String FMS_ACCOUNT_SET_MEMBER_UPDATE_SUB_TYPE = "更新账套成员"; + String FMS_ACCOUNT_SET_MEMBER_UPDATE_SUCCESS = + "更新了账套【{{#accountSet.companyName}}】的成员授权"; + + // ======================= FMS_FINANCE_PARAMETER 财务参数 ======================= + + String FMS_FINANCE_PARAMETER_TYPE = "FMS 财务参数"; + String FMS_FINANCE_PARAMETER_UPDATE_SUB_TYPE = "更新财务参数"; + String FMS_FINANCE_PARAMETER_UPDATE_SUCCESS = + "更新了账套【{{#accountSet.companyName}}】的财务参数: {_DIFF{#updateReqVO}}"; + + // ======================= FMS_SUBJECT 科目 ======================= + + String FMS_SUBJECT_TYPE = "FMS 科目"; + String FMS_SUBJECT_CREATE_SUB_TYPE = "创建科目"; + String FMS_SUBJECT_CREATE_SUCCESS = "创建了科目【{{#createReqVO.code}} {{#createReqVO.name}}】"; + String FMS_SUBJECT_UPDATE_SUB_TYPE = "更新科目"; + String FMS_SUBJECT_UPDATE_SUCCESS = "更新了科目【{{#updateReqVO.code}} {{#updateReqVO.name}}】"; + String FMS_SUBJECT_DELETE_SUB_TYPE = "删除科目"; + String FMS_SUBJECT_DELETE_SUCCESS = "删除了【{{#subjects.size()}}】个科目"; + String FMS_SUBJECT_STATUS_SUB_TYPE = "更新科目状态"; + String FMS_SUBJECT_STATUS_SUCCESS = "更新了科目状态"; + + // ======================= FMS_AUXILIARY 辅助核算 ======================= + + String FMS_AUXILIARY_TYPE = "FMS 辅助核算"; + String FMS_AUXILIARY_TYPE_CREATE_SUB_TYPE = "创建辅助核算分类"; + String FMS_AUXILIARY_TYPE_CREATE_SUCCESS = "创建了辅助核算分类【{{#createReqVO.name}}】"; + String FMS_AUXILIARY_TYPE_UPDATE_SUB_TYPE = "更新辅助核算分类"; + String FMS_AUXILIARY_TYPE_UPDATE_SUCCESS = "更新了辅助核算分类【{{#updateReqVO.name}}】"; + String FMS_AUXILIARY_TYPE_DELETE_SUB_TYPE = "删除辅助核算分类"; + String FMS_AUXILIARY_TYPE_DELETE_SUCCESS = "删除了辅助核算分类【{{#auxiliaryType.name}}】"; + + String FMS_AUXILIARY_ITEM_TYPE = "FMS 辅助核算项目"; + String FMS_AUXILIARY_ITEM_CREATE_SUB_TYPE = "创建辅助核算项目"; + String FMS_AUXILIARY_ITEM_CREATE_SUCCESS = "创建了辅助核算项目【{{#createReqVO.code}} {{#createReqVO.name}}】"; + String FMS_AUXILIARY_ITEM_UPDATE_SUB_TYPE = "更新辅助核算项目"; + String FMS_AUXILIARY_ITEM_UPDATE_SUCCESS = "更新了辅助核算项目【{{#updateReqVO.code}} {{#updateReqVO.name}}】"; + String FMS_AUXILIARY_ITEM_DELETE_SUB_TYPE = "删除辅助核算项目"; + String FMS_AUXILIARY_ITEM_DELETE_SUCCESS = "删除了辅助核算项目【{{#auxiliaryItem.code}} {{#auxiliaryItem.name}}】"; + String FMS_AUXILIARY_ITEM_STATUS_SUB_TYPE = "更新辅助核算项目状态"; + String FMS_AUXILIARY_ITEM_STATUS_SUCCESS = "更新了辅助核算项目状态"; + + // ======================= FMS_VOUCHER 凭证 ======================= + + String FMS_VOUCHER_TYPE = "FMS 凭证"; + String FMS_VOUCHER_CREATE_SUB_TYPE = "创建凭证"; + String FMS_VOUCHER_CREATE_SUCCESS = "创建了凭证【{{#voucherId}}】"; + String FMS_VOUCHER_UPDATE_SUB_TYPE = "更新凭证"; + String FMS_VOUCHER_UPDATE_SUCCESS = "更新了凭证【{{#updateReqVO.id}}】"; + String FMS_VOUCHER_ATTACHMENT_UPDATE_SUB_TYPE = "更新凭证附件"; + String FMS_VOUCHER_ATTACHMENT_UPDATE_SUCCESS = "更新了凭证【{{#updateReqVO.id}}】的附件"; + String FMS_VOUCHER_DELETE_SUB_TYPE = "删除凭证"; + String FMS_VOUCHER_DELETE_SUCCESS = "删除了凭证【{{#voucher.id}}】"; + String FMS_VOUCHER_REVIEW_SUB_TYPE = "审核凭证"; + String FMS_VOUCHER_REVIEW_SUCCESS = "更新了凭证审核状态"; + String FMS_VOUCHER_TIDY_SUB_TYPE = "整理凭证"; + String FMS_VOUCHER_TIDY_SUCCESS = "整理了【{{#tidyReqVO.month}}】期间的凭证"; + String FMS_VOUCHER_MOVE_SUB_TYPE = "移动凭证"; + String FMS_VOUCHER_MOVE_SUCCESS = "将【{{#moveReqVO.month}}】期间的【{{#moveReqVO.sourceNumber}}】号凭证移动到【{{#moveReqVO.targetNumber}}】号之前"; + + // ======================= FMS_VOUCHER_WORD 凭证字 ======================= + + String FMS_VOUCHER_WORD_TYPE = "FMS 凭证字"; + String FMS_VOUCHER_WORD_CREATE_SUB_TYPE = "创建凭证字"; + String FMS_VOUCHER_WORD_CREATE_SUCCESS = "创建了凭证字【{{#createReqVO.name}}】"; + String FMS_VOUCHER_WORD_UPDATE_SUB_TYPE = "更新凭证字"; + String FMS_VOUCHER_WORD_UPDATE_SUCCESS = "更新了凭证字【{{#updateReqVO.name}}】"; + String FMS_VOUCHER_WORD_DELETE_SUB_TYPE = "删除凭证字"; + String FMS_VOUCHER_WORD_DELETE_SUCCESS = "删除了凭证字【{{#voucherWord.name}}】"; + + // ======================= FMS_VOUCHER_TEMPLATE 凭证模板 ======================= + + String FMS_VOUCHER_TEMPLATE_CATEGORY_TYPE = "FMS 凭证模板分类"; + String FMS_VOUCHER_TEMPLATE_CATEGORY_CREATE_SUB_TYPE = "创建凭证模板分类"; + String FMS_VOUCHER_TEMPLATE_CATEGORY_CREATE_SUCCESS = "创建了凭证模板分类【{{#createReqVO.name}}】"; + String FMS_VOUCHER_TEMPLATE_CATEGORY_UPDATE_SUB_TYPE = "更新凭证模板分类"; + String FMS_VOUCHER_TEMPLATE_CATEGORY_UPDATE_SUCCESS = "更新了凭证模板分类【{{#updateReqVO.name}}】"; + String FMS_VOUCHER_TEMPLATE_CATEGORY_DELETE_SUB_TYPE = "删除凭证模板分类"; + String FMS_VOUCHER_TEMPLATE_CATEGORY_DELETE_SUCCESS = "删除了凭证模板分类【{{#category.name}}】"; + + String FMS_VOUCHER_TEMPLATE_TYPE = "FMS 凭证模板"; + String FMS_VOUCHER_TEMPLATE_CREATE_SUB_TYPE = "创建凭证模板"; + String FMS_VOUCHER_TEMPLATE_CREATE_SUCCESS = "创建了凭证模板【{{#createReqVO.name}}】"; + String FMS_VOUCHER_TEMPLATE_UPDATE_SUB_TYPE = "更新凭证模板"; + String FMS_VOUCHER_TEMPLATE_UPDATE_SUCCESS = "更新了凭证模板【{{#updateReqVO.name}}】"; + String FMS_VOUCHER_TEMPLATE_DELETE_SUB_TYPE = "删除凭证模板"; + String FMS_VOUCHER_TEMPLATE_DELETE_SUCCESS = "删除了凭证模板【{{#template.name}}】"; + + // ======================= FMS_DIGEST 常用摘要 ======================= + + String FMS_DIGEST_TYPE = "FMS 常用摘要"; + String FMS_DIGEST_CREATE_SUB_TYPE = "创建常用摘要"; + String FMS_DIGEST_CREATE_SUCCESS = "创建了常用摘要【{{#createReqVO.content}}】"; + String FMS_DIGEST_UPDATE_SUB_TYPE = "更新常用摘要"; + String FMS_DIGEST_UPDATE_SUCCESS = "更新了常用摘要【{{#updateReqVO.content}}】"; + String FMS_DIGEST_DELETE_SUB_TYPE = "删除常用摘要"; + String FMS_DIGEST_DELETE_SUCCESS = "删除了常用摘要【{{#digest.content}}】"; + + // ======================= FMS_CURRENCY 币别 ======================= + + String FMS_CURRENCY_TYPE = "FMS 币别"; + String FMS_CURRENCY_CREATE_SUB_TYPE = "创建币别"; + String FMS_CURRENCY_CREATE_SUCCESS = "创建了币别【{{#createReqVO.name}}】"; + String FMS_CURRENCY_UPDATE_SUB_TYPE = "更新币别"; + String FMS_CURRENCY_UPDATE_SUCCESS = "更新了币别【{{#updateReqVO.name}}】"; + String FMS_CURRENCY_DELETE_SUB_TYPE = "删除币别"; + String FMS_CURRENCY_DELETE_SUCCESS = "删除了币别【{{#currency.name}}】"; + + // ======================= FMS_INITIAL_BALANCE 财务初始余额 ======================= + + String FMS_INITIAL_BALANCE_TYPE = "FMS 财务初始余额"; + String FMS_INITIAL_BALANCE_SAVE_SUB_TYPE = "保存财务初始余额"; + String FMS_INITIAL_BALANCE_SAVE_SUCCESS = + "保存了账套【{{#saveReqVO.accountSetId}}】的【{{#saveReqVO.balances.size()}}】项财务初始余额"; + String FMS_INITIAL_BALANCE_IMPORT_SUB_TYPE = "导入财务初始余额"; + String FMS_INITIAL_BALANCE_IMPORT_SUCCESS = + "向账套【{{#accountSetId}}】导入了【{{#_ret}}】项财务初始余额"; + + // ======================= FMS_CLOSING 结账 ======================= + + String FMS_CLOSING_TYPE = "FMS 结账"; + String FMS_CLOSING_SCHEME_CREATE_SUB_TYPE = "创建结账方案"; + String FMS_CLOSING_SCHEME_CREATE_SUCCESS = "创建了结账方案【{{#createReqVO.name}}】"; + String FMS_CLOSING_SCHEME_UPDATE_SUB_TYPE = "更新结账方案"; + String FMS_CLOSING_SCHEME_UPDATE_SUCCESS = "更新了结账方案【{{#updateReqVO.name}}】"; + String FMS_CLOSING_SCHEME_DELETE_SUB_TYPE = "删除结账方案"; + String FMS_CLOSING_SCHEME_DELETE_SUCCESS = "删除了结账方案【{{#id}}】"; + String FMS_CLOSING_TEMPLATE_CREATE_SUB_TYPE = "创建结账模板"; + String FMS_CLOSING_TEMPLATE_CREATE_SUCCESS = "创建了结账模板【{{#createReqVO.name}}】"; + String FMS_CLOSING_TEMPLATE_UPDATE_SUB_TYPE = "更新结账模板"; + String FMS_CLOSING_TEMPLATE_UPDATE_SUCCESS = "更新了结账模板【{{#updateReqVO.name}}】"; + String FMS_CLOSING_TEMPLATE_DELETE_SUB_TYPE = "删除结账模板"; + String FMS_CLOSING_TEMPLATE_DELETE_SUCCESS = "删除了结账模板【{{#id}}】"; + String FMS_CLOSING_SETTINGS_UPDATE_SUB_TYPE = "更新结账设置"; + String FMS_CLOSING_SETTINGS_UPDATE_SUCCESS = "更新了账套【{{#saveReqVO.accountSetId}}】的结账设置"; + String FMS_CLOSING_SPECIAL_SETTINGS_UPDATE_SUCCESS = + "更新了账套【{{#updateReqVO.accountSetId}}】的结账方案【{{#updateReqVO.id}}】设置"; + String FMS_CLOSING_VOUCHER_GENERATE_SUB_TYPE = "生成结转凭证"; + String FMS_CLOSING_PROFIT_LOSS_VOUCHER_GENERATE_SUCCESS = + "生成了账套【{{#generateReqVO.accountSetId}}】{{#generateReqVO.month}}期间的结转损益凭证【{{#_ret}}】"; + String FMS_CLOSING_SCHEME_VOUCHER_GENERATE_SUCCESS = + "生成了账套【{{#generateReqVO.accountSetId}}】{{#generateReqVO.month}}期间方案【{{#generateReqVO.id}}】的结转凭证【{{#_ret}}】"; + String FMS_CLOSING_VOUCHER_BATCH_GENERATE_SUCCESS = + "为账套【{{#generateReqVO.accountSetId}}】{{#generateReqVO.month}}期间批量生成了【{{#_ret.size()}}】张结转凭证"; + String FMS_CLOSING_PERIOD_CLOSE_SUB_TYPE = "结账"; + String FMS_CLOSING_PERIOD_CLOSE_SUCCESS = + "完成了账套【{{#queryReqVO.accountSetId}}】{{#queryReqVO.month}}期间结账"; + String FMS_CLOSING_PERIOD_CANCEL_SUB_TYPE = "反结账"; + String FMS_CLOSING_PERIOD_CANCEL_SUCCESS = + "完成了账套【{{#queryReqVO.accountSetId}}】{{#queryReqVO.month}}期间反结账"; + + // ======================= FMS_CASH_FLOW_STATEMENT 现金流量表 ======================= + + String FMS_CASH_FLOW_STATEMENT_TYPE = "FMS 现金流量表"; + String FMS_CASH_FLOW_STATEMENT_UPDATE_SUB_TYPE = "调整现金流量表"; + String FMS_CASH_FLOW_STATEMENT_UPDATE_SUCCESS = + "调整了账套【{{#updateReqVO.accountSetId}}】{{#updateReqVO.startMonth}} 至 {{#updateReqVO.endMonth}}的现金流量表"; + String FMS_CASH_FLOW_ADJUSTMENT_UPDATE_SUB_TYPE = "调整现金流量辅助数据"; + String FMS_CASH_FLOW_ADJUSTMENT_UPDATE_SUCCESS = + "调整了账套【{{#updateReqVO.accountSetId}}】的【{{#updateReqVO.items.size()}}】项现金流量辅助数据"; + String FMS_CASH_FLOW_ADJUSTMENT_FORMULA_UPDATE_SUB_TYPE = "更新现金流量辅助公式"; + String FMS_CASH_FLOW_ADJUSTMENT_FORMULA_UPDATE_SUCCESS = + "更新了账套【{{#updateReqVO.accountSetId}}】的现金流量辅助公式【{{#updateReqVO.id}}】"; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingPresetSubjectEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingPresetSubjectEnum.java new file mode 100644 index 0000000..bb3a28e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingPresetSubjectEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.fms.enums.closing; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * FMS 预置结转科目编码枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsClosingPresetSubjectEnum { + + PRIOR_YEAR_ADJUSTMENT("6000", "以前年度损益调整"), + PRIOR_YEAR_ADJUSTMENT_CLOSING("310415", "未分配利润"), + CURRENT_YEAR_PROFIT("3103", "本年利润"), + + UNPAID_VAT("22210104", "转出未交增值税"), + OUTPUT_VAT("222102", "销项税额"), + VAT_PAYABLE("222101", "应交增值税"), + CONSUMPTION_TAX("222121", "应交消费税"), + + EDUCATION_SURCHARGE("540310", "教育费附加"), + EDUCATION_SURCHARGE_PAYABLE("222113", "应交教育费附加"), + CITY_MAINTENANCE_TAX("540303", "城市维护建设税"), + CITY_MAINTENANCE_TAX_PAYABLE("222117", "应交城市维护建设税"), + LOCAL_EDUCATION_SURCHARGE("540313", "地方教育费附加"), + LOCAL_EDUCATION_SURCHARGE_PAYABLE("222114", "应交地方教育费附加"), + + INCOME_TAX_EXPENSE("5801", "所得税费用"), + INCOME_TAX_PAYABLE("222111", "应交所得税"); + + /** + * 科目编码 + */ + private final String code; + /** + * 科目名称 + */ + private final String name; +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTemplateCategoryEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTemplateCategoryEnum.java new file mode 100644 index 0000000..a55afd7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTemplateCategoryEnum.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.fms.enums.closing; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 结账模板分类枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsClosingTemplateCategoryEnum implements ArrayValuable { + + DAILY_EXPENSE(1, "日常开支"), + PURCHASE_SALE(2, "采购销售"), + CURRENT_ACCOUNT(3, "往来款"), + TRANSFER_BUSINESS(4, "转账业务"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsClosingTemplateCategoryEnum::getCategory).toArray(Integer[]::new); + + /** + * 分类 + */ + private final Integer category; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTimeTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTimeTypeEnum.java new file mode 100644 index 0000000..c87a73e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTimeTypeEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.fms.enums.closing; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 结转取数时间枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsClosingTimeTypeEnum implements ArrayValuable { + + PERIOD_END(1, "期末"), + PERIOD_BEGIN(2, "期初"), + YEAR_BEGIN(3, "年初"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsClosingTimeTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsClosingTimeTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTypeEnum.java new file mode 100644 index 0000000..66cb9c3 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingTypeEnum.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.fms.enums.closing; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 结账业务类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsClosingTypeEnum implements ArrayValuable { + + REGULAR(1, "常规结账", null), + PROFIT_LOSS(2, "结转损益", "结转本期损益"), + UNPAID_VAT(3, "转出未交增值税", "转出未交增值税"), + LOCAL_TAX(4, "计提地税", "计提地税"), + INCOME_TAX(5, "计提所得税", "计提所得税"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsClosingTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + /** + * 默认凭证摘要 + */ + private final String digest; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsClosingTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingVoucherTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingVoucherTypeEnum.java new file mode 100644 index 0000000..aefddd5 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/closing/FmsClosingVoucherTypeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.enums.closing; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 结转凭证类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsClosingVoucherTypeEnum implements ArrayValuable { + + SEPARATE_GAIN_AND_LOSS(1, "收益和损失分开结转"), + COMBINED_GAIN_AND_LOSS(2, "收益和损失同时结转"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsClosingVoucherTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsClosingVoucherTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/common/FmsBalanceDirectionEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/common/FmsBalanceDirectionEnum.java new file mode 100644 index 0000000..5ed9662 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/common/FmsBalanceDirectionEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.fms.enums.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.math.BigDecimal; + +/** + * FMS 余额方向枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsBalanceDirectionEnum { + + DEBIT("借"), + CREDIT("贷"), + FLAT("平"); + + /** + * 名字 + */ + private final String name; + + public static FmsBalanceDirectionEnum valueOf(BigDecimal balance) { + return balance.signum() > 0 ? DEBIT : balance.signum() < 0 ? CREDIT : FLAT; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/common/FmsDebitCreditDirectionEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/common/FmsDebitCreditDirectionEnum.java new file mode 100644 index 0000000..66cb764 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/common/FmsDebitCreditDirectionEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.fms.enums.common; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 方向枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsDebitCreditDirectionEnum implements ArrayValuable { + + DEBIT(1, "借"), + CREDIT(2, "贷"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsDebitCreditDirectionEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsDebitCreditDirectionEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + public static FmsDebitCreditDirectionEnum valueOfName(String name) { + return ArrayUtil.firstMatch(item -> item.getName().equals(name), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAccountUserLevelEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAccountUserLevelEnum.java new file mode 100644 index 0000000..ff93ac4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAccountUserLevelEnum.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 账套用户权限级别枚举 + * + * OWNER > WRITE > READ + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsAccountUserLevelEnum implements ArrayValuable { + + OWNER(1, "主管"), + READ(2, "查看者"), + WRITE(3, "会计"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsAccountUserLevelEnum::getLevel).toArray(Integer[]::new); + + /** + * 级别 + */ + private final Integer level; + /** + * 级别名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isOwner(Integer level) { + return ObjUtil.equal(OWNER.level, level); + } + + public static boolean isRead(Integer level) { + return ObjUtil.equal(READ.level, level); + } + + public static boolean isWrite(Integer level) { + return ObjUtil.equal(WRITE.level, level); + } + + /** + * 判断是否可读 + * + * @param level 权限级别 + * @return 是否可读 + */ + public static boolean isReadable(Integer level) { + return valueOf(level) != null; + } + + /** + * 判断是否可写 + * + * @param level 权限级别 + * @return 是否可写 + */ + public static boolean isWritable(Integer level) { + return isOwner(level) || isWrite(level); + } + + public static FmsAccountUserLevelEnum valueOf(Integer level) { + return CollUtil.findOne(CollUtil.newArrayList(values()), + item -> ObjUtil.equal(item.level, level)); + } + + public static String getNameByLevel(Integer level) { + FmsAccountUserLevelEnum levelEnum = valueOf(level); + return levelEnum == null ? null : levelEnum.getName(); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAccountingStandardEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAccountingStandardEnum.java new file mode 100644 index 0000000..d80fbeb --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAccountingStandardEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 会计制度枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsAccountingStandardEnum implements ArrayValuable { + + SMALL_BUSINESS_2013(1, "小企业会计准则(2013 年颁)"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsAccountingStandardEnum::getStandard).toArray(Integer[]::new); + + /** + * 会计制度 + */ + private final Integer standard; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsAccountingStandardEnum valueOf(Integer standard) { + return ArrayUtil.firstMatch(item -> item.getStandard().equals(standard), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAuxiliaryTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAuxiliaryTypeEnum.java new file mode 100644 index 0000000..8e6032f --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsAuxiliaryTypeEnum.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 辅助核算类别枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsAuxiliaryTypeEnum implements ArrayValuable { + + CUSTOMER(1, "客户"), + SUPPLIER(2, "供应商"), + EMPLOYEE(3, "职员"), + PROJECT(4, "项目"), + DEPARTMENT(5, "部门"), + INVENTORY(6, "存货"), + CUSTOM(7, "自定义"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsAuxiliaryTypeEnum::getType).toArray(Integer[]::new); + + public static final FmsAuxiliaryTypeEnum[] SYSTEM_PRESET_TYPES = Arrays.stream(values()) + .filter(type -> type != CUSTOM).toArray(FmsAuxiliaryTypeEnum[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsAuxiliaryTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsCurrencyPresetEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsCurrencyPresetEnum.java new file mode 100644 index 0000000..e3c56f2 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsCurrencyPresetEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.math.BigDecimal; +import java.util.Arrays; + +/** + * FMS 预置币别枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsCurrencyPresetEnum implements ArrayValuable { + + RMB("RMB", "人民币", BigDecimal.ONE); + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(FmsCurrencyPresetEnum::getCode).toArray(String[]::new); + + /** + * 币别编码 + */ + private final String code; + /** + * 币别名称 + */ + private final String name; + /** + * 汇率 + */ + private final BigDecimal exchangeRate; + + @Override + public String[] array() { + return ARRAYS; + } + + public static FmsCurrencyPresetEnum valueOfCode(String code) { + return ArrayUtil.firstMatch(currency -> currency.getCode().equals(code), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsFinanceIndicatorTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsFinanceIndicatorTypeEnum.java new file mode 100644 index 0000000..4b99eac --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsFinanceIndicatorTypeEnum.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +// TODO DONE @AI:JSON 预置文件已格式化,枚举已归入 config 包。 +/** + * FMS 首页财务指标取数报表类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsFinanceIndicatorTypeEnum implements ArrayValuable { + + BALANCE_SHEET(1, "资产负债表"), + INCOME_STATEMENT(2, "利润表"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsFinanceIndicatorTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsFinanceIndicatorTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.type.equals(type), values()); + } + + public static boolean isBalanceSheet(Integer type) { + return BALANCE_SHEET.getType().equals(type); + } + + public static boolean isIncomeStatement(Integer type) { + return INCOME_STATEMENT.getType().equals(type); + } +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsSubjectCategoryEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsSubjectCategoryEnum.java new file mode 100644 index 0000000..81ce7e7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsSubjectCategoryEnum.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 科目类别枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsSubjectCategoryEnum { + + CURRENT_ASSET(1, 1, "流动资产"), + NON_CURRENT_ASSET(1, 2, "非流动资产"), + CURRENT_LIABILITY(2, 1, "流动负债"), + NON_CURRENT_LIABILITY(2, 2, "非流动负债"), + EQUITY(3, 1, "所有者权益"), + COST(4, 1, "成本"), + OPERATING_INCOME(5, 1, "营业收入"), + OTHER_INCOME(5, 2, "其他收益"), + PERIOD_EXPENSE(5, 3, "期间费用"), + OTHER_LOSS(5, 4, "其他损失"), + OPERATING_COST_AND_TAX(5, 5, "营业成本及税金"), + PRIOR_YEAR_ADJUSTMENT(5, 6, "以前年度损益调整"), + INCOME_TAX(5, 7, "所得税"), + COMMON(6, 1, "共同"); + + /** + * 科目类型 + */ + private final Integer type; + /** + * 类别 + */ + private final Integer category; + /** + * 名字 + */ + private final String name; + + public static FmsSubjectCategoryEnum valueOf(Integer type, Integer category) { + return Arrays.stream(values()) + .filter(item -> item.getType().equals(type) && item.getCategory().equals(category)) + .findFirst().orElse(null); + } + + public static FmsSubjectCategoryEnum valueOfName(String name) { + return Arrays.stream(values()) + .filter(item -> item.getName().equals(name)) + .findFirst().orElse(null); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsSubjectTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsSubjectTypeEnum.java new file mode 100644 index 0000000..e7996f7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsSubjectTypeEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 科目类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsSubjectTypeEnum implements ArrayValuable { + + ASSET(1, "资产"), + LIABILITY(2, "负债"), + EQUITY(3, "所有者权益"), + COST(4, "成本"), + PROFIT_LOSS(5, "损益"), + COMMON(6, "共同"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsSubjectTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsSubjectTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsVoucherWordPresetEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsVoucherWordPresetEnum.java new file mode 100644 index 0000000..102257b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/config/FmsVoucherWordPresetEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.fms.enums.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * FMS 预置凭证字枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsVoucherWordPresetEnum { + + RECORD("记", "记账凭证", true, 1), + RECEIPT("收", "收款凭证", false, 2), + TRANSFER("转", "转账凭证", false, 3), + PAYMENT("付", "付款凭证", false, 4); + + /** + * 凭证字 + */ + private final String name; + /** + * 打印标题 + */ + private final String printTitle; + /** + * 是否默认凭证字 + */ + private final Boolean defaultStatus; + /** + * 显示顺序 + */ + private final Integer sort; + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/home/FmsHomeMetricEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/home/FmsHomeMetricEnum.java new file mode 100644 index 0000000..3ea004a --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/home/FmsHomeMetricEnum.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.fms.enums.home; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 首页财务指标枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsHomeMetricEnum { + + INCOME("income", "收入", new Integer[]{1}, "L1"), + OPERATING_COST("operatingCost", "成本", new Integer[]{2, 3}, "L2+L3"), + PROFIT("profit", "利润", new Integer[]{30}, "L30"), + EXPENSE("expense", "费用", new Integer[]{11, 14, 18}, "L11+L14+L18"), + OTHER("other", "其他", new Integer[]{}, "L30+L11+L14+L18+L2+L3-L1"); + + /** + * 指标标识 + */ + private final String key; + /** + * 指标名称 + */ + private final String name; + /** + * 利润表行次数组 + */ + private final Integer[] rowNumbers; + /** + * 科目构成行次公式 + */ + private final String structureFormula; + + public static FmsHomeMetricEnum valueOfKey(String key) { + return Arrays.stream(values()).filter(item -> item.getKey().equals(key)) + .findFirst().orElse(null); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/ledger/FmsLedgerBalanceModeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/ledger/FmsLedgerBalanceModeEnum.java new file mode 100644 index 0000000..4ea0e8b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/ledger/FmsLedgerBalanceModeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.enums.ledger; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 账簿余额方向模式枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsLedgerBalanceModeEnum implements ArrayValuable { + + SAME_AS_SUBJECT(1, "与科目方向相同"), + OPPOSITE_TO_SUBJECT(2, "与科目方向相反"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsLedgerBalanceModeEnum::getMode).toArray(Integer[]::new); + + /** + * 模式 + */ + private final Integer mode; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsLedgerBalanceModeEnum valueOf(Integer mode) { + return ArrayUtil.firstMatch(item -> item.getMode().equals(mode), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsCashFlowExtendTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsCashFlowExtendTypeEnum.java new file mode 100644 index 0000000..8aac640 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsCashFlowExtendTypeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.enums.report; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 现金流量扩展公式类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsCashFlowExtendTypeEnum implements ArrayValuable { + + CUSTOM(0, "用户配置"), + FIXED(1, "固定公式"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsCashFlowExtendTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsCashFlowExtendTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsCashFlowParamTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsCashFlowParamTypeEnum.java new file mode 100644 index 0000000..1ac3a5a --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsCashFlowParamTypeEnum.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.fms.enums.report; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * FMS 现金流量表跨报表取数参数类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsCashFlowParamTypeEnum { + + BALANCE("BA", "资产负债取数"), + INCOME("IN", "利润表取数"), + EXTEND("EX", "辅助数据取数"), + LINE("L", "行次取数"); + + /** + * 参数前缀 + */ + private final String prefix; + /** + * 名称 + */ + private final String name; + + /** + * 解析参数前缀对应的取数类型 + * + * @param param 参数,例如 BA[1,2]、IN32、EX4、L7 + * @return 取数类型,无法识别时返回 null + */ + public static FmsCashFlowParamTypeEnum of(String param) { + for (FmsCashFlowParamTypeEnum type : values()) { + if (StrUtil.startWith(param, type.getPrefix())) { + return type; + } + } + return null; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsFormulaRuleEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsFormulaRuleEnum.java new file mode 100644 index 0000000..e142ff0 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsFormulaRuleEnum.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.fms.enums.report; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 报表取数规则枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsFormulaRuleEnum implements ArrayValuable { + + BALANCE(0, "余额"), + DEBIT_BALANCE(1, "借方余额"), + CREDIT_BALANCE(2, "贷方余额"), + SUBJECT_DEBIT_BALANCE(3, "科目借方余额"), + SUBJECT_CREDIT_BALANCE(4, "科目贷方余额"), + DEBIT_AMOUNT(5, "借方发生额"), + CREDIT_AMOUNT(6, "贷方发生额"), + PROFIT_LOSS_AMOUNT(7, "损益发生额"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsFormulaRuleEnum::getRule).toArray(Integer[]::new); + + /** + * 规则 + */ + private final Integer rule; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsFormulaRuleEnum valueOf(Integer rule) { + return ArrayUtil.firstMatch(item -> item.getRule().equals(rule), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsReportPeriodTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsReportPeriodTypeEnum.java new file mode 100644 index 0000000..e4ed247 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsReportPeriodTypeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.enums.report; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 报表期间类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsReportPeriodTypeEnum implements ArrayValuable { + + MONTH(1, "月度"), + QUARTER(2, "季度"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsReportPeriodTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsReportPeriodTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsReportTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsReportTypeEnum.java new file mode 100644 index 0000000..4cdd419 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/report/FmsReportTypeEnum.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.fms.enums.report; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 财务报表类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsReportTypeEnum implements ArrayValuable { + + BALANCE_SHEET(1, "资产负债表"), + INCOME_STATEMENT(2, "利润表"), + CASH_FLOW_STATEMENT(3, "现金流量表"), + CASH_FLOW_ADJUSTMENT(4, "现金流量辅助数据"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsReportTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsReportTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/voucher/FmsVoucherStatusEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/voucher/FmsVoucherStatusEnum.java new file mode 100644 index 0000000..d1353f1 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/voucher/FmsVoucherStatusEnum.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.fms.enums.voucher; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 凭证状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsVoucherStatusEnum implements ArrayValuable { + + PENDING_REVIEW(0, "待审核"), + APPROVED(1, "已审核"), + REJECTED(2, "已驳回"), + REVIEWING(3, "审核中"), + WITHDRAWN(4, "已撤回"), + UNSUBMITTED(5, "未提交"), + CREATED(6, "已创建"), + DELETED(7, "已删除"), + VOIDED(8, "已作废"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsVoucherStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsVoucherStatusEnum valueOf(Integer status) { + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/voucher/FmsVoucherTidyTypeEnum.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/voucher/FmsVoucherTidyTypeEnum.java new file mode 100644 index 0000000..9afa176 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/enums/voucher/FmsVoucherTidyTypeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.enums.voucher; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * FMS 凭证整理方式枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum FmsVoucherTidyTypeEnum implements ArrayValuable { + + FILL_GAPS(1, "按凭证号顺次前移补齐断号"), + REORDER_BY_TIME(2, "按凭证日期重新顺次编号"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(FmsVoucherTidyTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static FmsVoucherTidyTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/package-info.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/package-info.java new file mode 100644 index 0000000..f534bcb --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 fms 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.fms.framework; diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/web/config/FmsWebConfiguration.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/web/config/FmsWebConfiguration.java new file mode 100644 index 0000000..130fe47 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/web/config/FmsWebConfiguration.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.fms.framework.web.config; + +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * fms 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class FmsWebConfiguration { + + /** + * fms 模块的 API 分组 + * + * @return fms 模块的 OpenAPI 分组 + */ + @Bean + public GroupedOpenApi fmsGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("fms"); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/web/package-info.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/web/package-info.java new file mode 100644 index 0000000..b2eab0b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * fms 模块的 web 拓展封装 + */ +package cn.iocoder.yudao.module.fms.framework.web; diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/package-info.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/package-info.java new file mode 100644 index 0000000..52a71fc --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/package-info.java @@ -0,0 +1,4 @@ +/** + * FMS 财务管理模块,包含凭证、账簿、报表和结账等业务能力 + */ +package cn.iocoder.yudao.module.fms; diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodService.java new file mode 100644 index 0000000..b6bb20d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodService.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; + +import java.time.LocalDateTime; +import java.time.YearMonth; + +/** + * FMS 结账期间 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsClosingPeriodService { + + /** + * 校验业务时间所属期间未结账 + * + * @param accountSetId 账套编号 + * @param businessTime 业务时间 + */ + void validatePeriodOpen(Long accountSetId, LocalDateTime businessTime); + + /** + * 判断业务时间所属期间是否已结账 + * + * @param accountSetId 账套编号 + * @param businessTime 业务时间 + * @return 是否已结账 + */ + boolean isPeriodClosed(Long accountSetId, LocalDateTime businessTime); + + /** + * 获得当前会计期间 + * + * @param accountSetId 账套编号 + * @param startTime 账套启用时间 + * @return 当前会计期间 + */ + YearMonth getCurrentMonth(Long accountSetId, LocalDateTime startTime); + + /** + * 获得当前会计期间 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 当前会计期间 + */ + String getCurrentMonth(Long accountSetId, Long userId); + + /** + * 获得结账概况 + * + * @param queryReqVO 查询参数 + * @param userId 用户编号 + * @return 结账概况 + */ + FmsClosingOverviewRespVO getClosingOverview(FmsClosingQueryReqVO queryReqVO, Long userId); + + /** + * 结账 + * + * @param queryReqVO 结账期间 + * @param userId 用户编号 + */ + void closePeriod(FmsClosingQueryReqVO queryReqVO, Long userId); + + /** + * 反结账 + * + * @param queryReqVO 结账期间 + * @param userId 用户编号 + */ + void cancelClosePeriod(FmsClosingQueryReqVO queryReqVO, Long userId); + + /** + * 判断指定会计期间是否已结账 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @return 是否已结账 + */ + boolean isPeriodClosed(Long accountSetId, String month); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodServiceImpl.java new file mode 100644 index 0000000..207c930 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodServiceImpl.java @@ -0,0 +1,510 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.income.FmsIncomeStatementCheckRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingPeriodDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingPeriodMapper; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherStatusEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.report.FmsBalanceSheetService; +import cn.iocoder.yudao.module.fms.service.report.FmsBalanceSheetServiceImpl; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.count; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; + +/** + * FMS 结账期间 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsClosingPeriodServiceImpl implements FmsClosingPeriodService { + + @Resource + private FmsClosingPeriodMapper closingPeriodMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsClosingSchemeService closingSchemeService; + @Resource + private FmsClosingVoucherService closingVoucherService; + @Resource + private FmsFinanceParameterService financeParameterService; + @Resource + private FmsInitialBalanceService initialBalanceService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsLedgerService ledgerService; + @Resource + private FmsVoucherService voucherService; + @Resource + private FmsBalanceSheetService balanceSheetService; + @Resource + private FmsIncomeStatementService incomeStatementService; + + @Override + public void validatePeriodOpen(Long accountSetId, LocalDateTime businessTime) { + if (isPeriodClosed(accountSetId, businessTime)) { + throw exception(CLOSING_PERIOD_CLOSED); + } + } + + @Override + public boolean isPeriodClosed(Long accountSetId, LocalDateTime businessTime) { + YearMonth month = YearMonth.from(businessTime); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + return closingPeriodMapper.selectByPeriod(accountSetId, monthBeginTime, + LocalDateTimeUtils.endOfMonth(monthBeginTime)) != null; + } + + @Override + public YearMonth getCurrentMonth(Long accountSetId, LocalDateTime startTime) { + FmsClosingPeriodDO latestPeriod = closingPeriodMapper.selectLatestByAccountSetId(accountSetId); + return latestPeriod != null ? YearMonth.from(latestPeriod.getClosingTime()).plusMonths(1) + : YearMonth.from(startTime); + } + + @Override + public String getCurrentMonth(Long accountSetId, Long userId) { + // 1.1 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 校验账套初始化状态 + if (ObjUtil.notEqual(accountSet.getInitialized(), true) || accountSet.getStartTime() == null) { + throw exception(ACCOUNT_SET_NOT_INITIALIZED); + } + + // 2. 获得当前会计期间 + return getCurrentMonth(accountSetId, accountSet.getStartTime()).toString(); + } + + @Override + public FmsClosingOverviewRespVO getClosingOverview( + FmsClosingQueryReqVO queryReqVO, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(queryReqVO.getAccountSetId(), userId); + // 1.2 解析会计期间 + YearMonth month = LocalDateTimeUtils.parseYearMonth(queryReqVO.getMonth()); + LocalDateTime beginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime endTime = LocalDateTimeUtils.endOfMonth(beginTime); + + // 2. 查询结账前检查项 + List vouchers = voucherService.getVoucherListByPeriod( + queryReqVO.getAccountSetId(), beginTime, endTime); + List closingVouchers = closingVoucherService.getClosingVoucherListByPeriod( + queryReqVO.getAccountSetId(), beginTime, endTime); + long pendingVoucherCount = getPendingVoucherCount(vouchers, closingVouchers); + long voucherCount = vouchers.size(); + BigDecimal profitLossBalance = calculateProfitLossBalance(queryReqVO.getAccountSetId(), month, userId); + BigDecimal balanceSheetDifference = calculateBalanceSheetDifference( + queryReqVO.getAccountSetId(), month, userId); + FmsClosingSchemeDO profitLossClosing = closingSchemeService.getClosingSchemeByAccountSetIdAndType( + queryReqVO.getAccountSetId(), FmsClosingTypeEnum.PROFIT_LOSS.getType()); + FmsClosingVoucherDO profitLossVoucher = profitLossClosing == null ? null + : CollUtil.findOne(closingVouchers, item -> + ObjUtil.equal(item.getClosingId(), profitLossClosing.getId())); + + // 3. 查询其余结账前检查项 + FmsTrialBalanceRespVO trialBalance = initialBalanceService.getTrialBalance( + queryReqVO.getAccountSetId(), userId); + boolean voucherNumberContinuous = isVoucherNumberContinuous(vouchers); + boolean profitLossVoucherGenerated = !hasProfitLossActivity( + queryReqVO.getAccountSetId(), month, userId) || profitLossVoucher != null; + FmsReportListReqVO reportReqVO = new FmsReportListReqVO().setAccountSetId(queryReqVO.getAccountSetId()) + .setStartMonth(queryReqVO.getMonth()).setEndMonth(queryReqVO.getMonth()); + FmsIncomeStatementCheckRespVO incomeCheck = incomeStatementService.checkIncomeStatement(reportReqVO, userId); + FmsBalanceSheetCheckRespVO balanceCheck = balanceSheetService.checkBalanceSheet(reportReqVO, userId); + + // 4. 组装结账概况 + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter( + queryReqVO.getAccountSetId(), userId); + boolean voucherReviewRequired = Boolean.TRUE.equals(financeParameter.getVoucherReviewRequired()); + boolean initialBalanceBalanced = trialBalance != null && Boolean.TRUE.equals(trialBalance.getBalanced()); + boolean incomeStatementBalanced = incomeCheck != null && Boolean.TRUE.equals(incomeCheck.getBalanced()); + int incomeUnmappedCount = incomeCheck == null || incomeCheck.getUnmappedSubjects() == null + ? 0 : incomeCheck.getUnmappedSubjects().size(); + boolean balanceSheetProfitLossTransferred = balanceCheck != null + && Boolean.TRUE.equals(balanceCheck.getProfitLossTransferred()); + boolean balanceSheetBalanced = balanceCheck != null && Boolean.TRUE.equals(balanceCheck.getBalanced()); + int balanceUnmappedCount = balanceCheck == null || balanceCheck.getUnmappedSubjects() == null + ? 0 : balanceCheck.getUnmappedSubjects().size(); + boolean closed = closingPeriodMapper.selectByPeriod( + queryReqVO.getAccountSetId(), beginTime, endTime) != null; + boolean canClose = !closed + && (!voucherReviewRequired || pendingVoucherCount == 0) + && initialBalanceBalanced && voucherNumberContinuous && profitLossVoucherGenerated + && profitLossBalance.compareTo(BigDecimal.ZERO) == 0 + && incomeStatementBalanced && incomeUnmappedCount == 0 + && balanceSheetProfitLossTransferred && balanceSheetBalanced && balanceUnmappedCount == 0; + return new FmsClosingOverviewRespVO().setMonth(queryReqVO.getMonth()) + .setClosed(closed) + .setVoucherReviewRequired(voucherReviewRequired) + .setPendingVoucherCount(pendingVoucherCount) + .setVoucherCount(voucherCount) + .setProfitLossBalance(profitLossBalance) + .setBalanceSheetDifference(balanceSheetDifference) + .setProfitLossVoucherId(profitLossVoucher == null ? null : profitLossVoucher.getVoucherId()) + .setInitialBalanceBalanced(initialBalanceBalanced) + .setVoucherNumberContinuous(voucherNumberContinuous) + .setProfitLossVoucherGenerated(profitLossVoucherGenerated) + .setIncomeStatementBalanced(incomeStatementBalanced) + .setIncomeStatementUnmappedSubjectCount(incomeUnmappedCount) + .setBalanceSheetProfitLossTransferred(balanceSheetProfitLossTransferred) + .setBalanceSheetBalanced(balanceSheetBalanced) + .setBalanceSheetUnmappedSubjectCount(balanceUnmappedCount) + .setCanClose(canClose); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_PERIOD_CLOSE_SUB_TYPE, + bizNo = "{{#queryReqVO.accountSetId}}", success = FMS_CLOSING_PERIOD_CLOSE_SUCCESS) + public void closePeriod(FmsClosingQueryReqVO queryReqVO, Long userId) { + // 1. 校验账套权限、初始化状态和结账目标期间 + YearMonth currentMonth = validateCloseTargetPeriod(queryReqVO.getAccountSetId(), queryReqVO.getMonth(), userId); + YearMonth targetMonth = LocalDateTimeUtils.parseYearMonth(queryReqVO.getMonth()); + + // 2. 逐月执行结账,保证目标期间之前的每个期间都已完成完整检查 + for (YearMonth month = currentMonth; !month.isAfter(targetMonth); month = month.plusMonths(1)) { + closeSinglePeriod(queryReqVO.getAccountSetId(), month, userId); + } + } + + /** + * 执行单个会计期间的结账检查和状态登记 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + */ + private void closeSinglePeriod(Long accountSetId, YearMonth month, Long userId) { + // 1. 查询结账前概况 + FmsClosingQueryReqVO queryReqVO = new FmsClosingQueryReqVO() + .setAccountSetId(accountSetId).setMonth(month.toString()); + // 1.2 校验结账概况 + FmsClosingOverviewRespVO overview = getClosingOverview(queryReqVO, userId); + if (overview.getClosed()) { + throw exception(CLOSING_PERIOD_CLOSED); + } + if (overview.getVoucherReviewRequired() && overview.getPendingVoucherCount() > 0) { + throw exception(CLOSING_VOUCHER_PENDING_REVIEW); + } + + // 2.1 校验初始余额试算平衡 + if (!Boolean.TRUE.equals(overview.getInitialBalanceBalanced())) { + throw exception(CLOSING_INITIAL_BALANCE_UNBALANCED); + } + // 2.2 校验凭证号连续 + if (!Boolean.TRUE.equals(overview.getVoucherNumberContinuous())) { + throw exception(CLOSING_VOUCHER_NUMBER_DISCONTINUOUS); + } + + // 3.1 校验损益结转凭证 + if (!Boolean.TRUE.equals(overview.getProfitLossVoucherGenerated())) { + throw exception(CLOSING_PROFIT_LOSS_VOUCHER_MISSING); + } + if (overview.getProfitLossBalance().compareTo(BigDecimal.ZERO) != 0) { + throw exception(CLOSING_PROFIT_LOSS_NOT_TRANSFERRED); + } + // 3.2 校验利润表勾稽和公式覆盖 + if (!Boolean.TRUE.equals(overview.getIncomeStatementBalanced())) { + throw exception(CLOSING_INCOME_STATEMENT_UNBALANCED); + } + if (overview.getIncomeStatementUnmappedSubjectCount() > 0) { + throw exception(CLOSING_REPORT_SUBJECT_UNMAPPED); + } + // 3.3 校验资产负债表平衡和公式覆盖 + if (!Boolean.TRUE.equals(overview.getBalanceSheetProfitLossTransferred())) { + throw exception(CLOSING_PROFIT_LOSS_NOT_TRANSFERRED); + } + if (!Boolean.TRUE.equals(overview.getBalanceSheetBalanced())) { + throw exception(CLOSING_BALANCE_SHEET_UNBALANCED); + } + if (overview.getBalanceSheetUnmappedSubjectCount() > 0) { + throw exception(CLOSING_REPORT_SUBJECT_UNMAPPED); + } + + // 4. 登记结账期间 + LocalDateTime beginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime endTime = LocalDateTimeUtils.endOfMonth(beginTime); + closingPeriodMapper.insert(new FmsClosingPeriodDO().setClosingTime(month.atEndOfMonth().atStartOfDay()) + .setAccountSetId(accountSetId)); + + // 5. 更新当期结转凭证为已结账 + closingVoucherService.updateClosingVoucherClosedByPeriod( + accountSetId, beginTime, endTime, true); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_PERIOD_CANCEL_SUB_TYPE, + bizNo = "{{#queryReqVO.accountSetId}}", success = FMS_CLOSING_PERIOD_CANCEL_SUCCESS) + public void cancelClosePeriod(FmsClosingQueryReqVO queryReqVO, Long userId) { + // 1.1 校验账套写权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetWritePermission( + queryReqVO.getAccountSetId(), userId); + // 1.2 串行化同一账套的结账状态变更 + accountSetService.lockAccountSet(queryReqVO.getAccountSetId()); + + // 2. 从最近期间逐月反结账到目标期间 + FmsClosingPeriodDO latest = closingPeriodMapper.selectLatestByAccountSetId(queryReqVO.getAccountSetId()); + YearMonth targetMonth = LocalDateTimeUtils.parseYearMonth(queryReqVO.getMonth()); + YearMonth startMonth = accountSet.getStartTime() == null ? null : YearMonth.from(accountSet.getStartTime()); + if (latest == null || targetMonth.isAfter(YearMonth.from(latest.getClosingTime())) + || (startMonth != null && targetMonth.isBefore(startMonth))) { + throw exception(CLOSING_PERIOD_NOT_CLOSED); + } + for (YearMonth month = YearMonth.from(latest.getClosingTime()); !month.isBefore(targetMonth); + month = month.minusMonths(1)) { + cancelSinglePeriod(queryReqVO.getAccountSetId(), month); + } + } + + /** + * 撤销单个会计期间的结账状态 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + */ + private void cancelSinglePeriod(Long accountSetId, YearMonth month) { + LocalDateTime beginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime endTime = LocalDateTimeUtils.endOfMonth(beginTime); + FmsClosingPeriodDO period = closingPeriodMapper.selectByPeriod(accountSetId, beginTime, endTime); + if (period == null) { + throw exception(CLOSING_PERIOD_NOT_CLOSED); + } + closingPeriodMapper.deleteById(period.getId()); + closingVoucherService.updateClosingVoucherClosedByPeriod(accountSetId, beginTime, endTime, false); + } + + @Override + public boolean isPeriodClosed(Long accountSetId, String monthValue) { + YearMonth month = LocalDateTimeUtils.parseYearMonth(monthValue); + LocalDateTime beginTime = LocalDateTimeUtils.getMonthBeginTime(month); + FmsClosingPeriodDO period = closingPeriodMapper.selectByPeriod(accountSetId, beginTime, LocalDateTimeUtils.endOfMonth(beginTime)); + return period != null; + } + + /** + * 获得待审核凭证数量 + * + * 结转凭证由结账流程生成且不提供人工审核入口,不计入结账前的待审核凭证数量 + * + * @param vouchers 会计期间凭证列表 + * @param closingVouchers 会计期间结转凭证列表 + * @return 待审核凭证数量 + */ + private long getPendingVoucherCount(List vouchers, + List closingVouchers) { + Set closingVoucherIds = convertSet(closingVouchers, FmsClosingVoucherDO::getVoucherId); + return count(vouchers, voucher -> + ObjUtil.equal(voucher.getStatus(), FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + && !closingVoucherIds.contains(voucher.getId())); + } + + /** + * 计算末级损益科目的本期借贷差额绝对值合计 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + * @return 损益余额 + */ + private BigDecimal calculateProfitLossBalance(Long accountSetId, YearMonth month, Long userId) { + // 1. 查询科目和当期科目余额 + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId, Objects::nonNull); + Map balanceMap = getSubjectBalanceMap(accountSetId, month, userId); + + // 2. 汇总末级损益科目的本期借贷差额 + BigDecimal result = BigDecimal.ZERO; + for (FmsSubjectDO subject : subjects) { + if (ObjUtil.notEqual(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || parentIds.contains(subject.getId())) { + continue; + } + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(subject.getId()); + if (balance != null) { + result = result.add(NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()) + .subtract(NumberUtils.zeroIfNull(balance.getPeriodCreditAmount())).abs()); + } + } + return result; + } + + /** + * 判断当期损益类末级科目是否存在发生额 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + * @return 是否存在发生额 + */ + private boolean hasProfitLossActivity(Long accountSetId, YearMonth month, Long userId) { + // 1. 查询末级损益科目和当期余额 + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId, Objects::nonNull); + Map balanceMap = getSubjectBalanceMap(accountSetId, month, userId); + + // 2. 任一末级损益科目存在借方或贷方发生额即视为当期有损益业务 + for (FmsSubjectDO subject : subjects) { + if (ObjUtil.notEqual(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || parentIds.contains(subject.getId())) { + continue; + } + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(subject.getId()); + if (balance != null && (NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()).signum() != 0 + || NumberUtils.zeroIfNull(balance.getPeriodCreditAmount()).signum() != 0)) { + return true; + } + } + return false; + } + + /** + * 校验每个凭证字的当期凭证号从 1 开始连续编号 + * + * @param vouchers 当期凭证列表 + * @return 是否连续编号 + */ + private boolean isVoucherNumberContinuous(List vouchers) { + // 1. 按凭证字分组 + Map> voucherMap = vouchers.stream() + .collect(Collectors.groupingBy(FmsVoucherDO::getVoucherWordId, LinkedHashMap::new, + Collectors.toList())); + + // 2. 校验各凭证字的号码从 1 开始连续递增 + for (List wordVouchers : voucherMap.values()) { + for (int index = 0; index < wordVouchers.size(); index++) { + if (ObjUtil.notEqual(wordVouchers.get(index).getVoucherNumber(), index + 1)) { + return false; + } + } + } + return true; + } + + private BigDecimal calculateBalanceSheetDifference(Long accountSetId, YearMonth month, Long userId) { + FmsReportListReqVO listReqVO = new FmsReportListReqVO().setAccountSetId(accountSetId) + .setStartMonth(month.toString()).setEndMonth(month.toString()); + BigDecimal assetAmount = BigDecimal.ZERO; + BigDecimal liabilityAmount = BigDecimal.ZERO; + for (FmsBalanceSheetRowRespVO row : balanceSheetService.getBalanceSheet(listReqVO, userId)) { + if (ObjUtil.equal(row.getAssetRowNo(), FmsBalanceSheetServiceImpl.ASSET_TOTAL_ROW_NO)) { + assetAmount = NumberUtils.zeroIfNull(row.getAssetClosingAmount()); + } + if (ObjUtil.equal(row.getLiabilityRowNo(), FmsBalanceSheetServiceImpl.LIABILITY_TOTAL_ROW_NO)) { + liabilityAmount = NumberUtils.zeroIfNull(row.getLiabilityClosingAmount()); + } + } + return assetAmount.subtract(liabilityAmount).abs(); + } + + /** + * 获得单个会计期间的科目余额 Map + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + * @return 科目编号到余额的 Map + */ + private Map getSubjectBalanceMap(Long accountSetId, YearMonth month, Long userId) { + return getSubjectBalanceMap(accountSetId, month, month, userId); + } + + /** + * 获得月份区间的科目余额 Map,树形余额扁平化后按科目编号存放 + * + * @param accountSetId 账套编号 + * @param startMonth 开始月份 + * @param endMonth 结束月份 + * @param userId 用户编号 + * @return 科目编号到余额的 Map + */ + private Map getSubjectBalanceMap(Long accountSetId, YearMonth startMonth, YearMonth endMonth, Long userId) { + FmsLedgerListReqVO listReqVO = new FmsLedgerListReqVO().setAccountSetId(accountSetId) + .setStartMonth(startMonth.toString()).setEndMonth(endMonth.toString()); + Map result = new LinkedHashMap<>(); + flattenBalances(ledgerService.getSubjectBalanceList(listReqVO, userId), result); + return result; + } + + /** + * 递归扁平化树形科目余额到 Map + * + * @param balances 树形科目余额 + * @param result 科目编号到余额的 Map + */ + private void flattenBalances(List balances, + Map result) { + for (FmsLedgerSubjectBalanceRespVO balance : balances) { + result.put(balance.getSubjectId(), balance); + flattenBalances(balance.getChildren(), result); + } + } + + /** + * 校验账套写权限、初始化状态和结账目标期间 + * + * @param accountSetId 账套编号 + * @param month 结账目标期间 + * @param userId 用户编号 + * @return 当前会计期间 + */ + private YearMonth validateCloseTargetPeriod(Long accountSetId, String month, Long userId) { + FmsAccountSetDO accountSet = accountSetService.validateAccountSetWritePermission(accountSetId, userId); + accountSetService.lockAccountSet(accountSetId); + if (ObjUtil.notEqual(accountSet.getInitialized(), true) || accountSet.getStartTime() == null) { + throw exception(ACCOUNT_SET_NOT_INITIALIZED); + } + YearMonth currentMonth = getCurrentMonth(accountSetId, accountSet.getStartTime()); + if (LocalDateTimeUtils.parseYearMonth(month).isBefore(currentMonth)) { + throw exception(CLOSING_NOT_CURRENT_PERIOD); + } + return currentMonth; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeService.java new file mode 100644 index 0000000..ed66e0c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeService.java @@ -0,0 +1,116 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossSettingsSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsSpecialClosingSettingsSaveReqVO; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 结账方案 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsClosingSchemeService { + + /** + * 初始化账套默认结账方案 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + */ + void initializeDefaultClosingSchemes(Long accountSetId, Long userId); + + /** + * 获得账套下指定类型的结账方案 + * + * @param accountSetId 账套编号 + * @param type 结账业务类型 + * @return 结账方案 + */ + FmsClosingSchemeDO getClosingSchemeByAccountSetIdAndType(Long accountSetId, Integer type); + + /** + * 校验账套下的结账方案存在 + * + * @param accountSetId 账套编号 + * @param id 方案编号 + * @return 结账方案 + */ + FmsClosingSchemeDO validateClosingSchemeExists(Long accountSetId, Long id); + + /** + * 获得引用指定科目的结账方案数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号数组 + * @return 结账方案数量 + */ + Long getClosingSchemeCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 获得引用指定凭证字的结账方案数量 + * + * @param accountSetId 账套编号 + * @param voucherWordId 凭证字编号 + * @return 结账方案数量 + */ + Long getClosingSchemeCountByVoucherWordId(Long accountSetId, Long voucherWordId); + + /** + * 保存结转损益设置 + * + * @param saveReqVO 保存参数 + * @param userId 用户编号 + * @return 方案编号 + */ + Long saveProfitLossSettings(FmsProfitLossSettingsSaveReqVO saveReqVO, Long userId); + + /** + * 获得结账方案列表 + * + * @param queryReqVO 查询参数 + * @param userId 用户编号 + * @return 结账方案列表 + */ + List getClosingSchemeList(FmsClosingQueryReqVO queryReqVO, Long userId); + + /** + * 创建结账方案 + * + * @param createReqVO 创建参数 + * @param userId 用户编号 + * @return 方案编号 + */ + Long createClosingScheme(FmsClosingSchemeSaveReqVO createReqVO, Long userId); + + /** + * 更新结账方案 + * + * @param updateReqVO 更新参数 + * @param userId 用户编号 + */ + void updateClosingScheme(FmsClosingSchemeSaveReqVO updateReqVO, Long userId); + + /** + * 更新专用结转设置 + * + * @param updateReqVO 更新参数 + * @param userId 用户编号 + */ + void updateSpecialClosingSettings(FmsSpecialClosingSettingsSaveReqVO updateReqVO, Long userId); + + /** + * 删除结账方案 + * + * @param accountSetId 账套编号 + * @param id 方案编号 + * @param userId 用户编号 + */ + void deleteClosingScheme(Long accountSetId, Long id, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeServiceImpl.java new file mode 100644 index 0000000..96bc110 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeServiceImpl.java @@ -0,0 +1,934 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossSettingsSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsSpecialClosingSettingsSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingSchemeMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingVoucherMapper; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingPresetSubjectEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingVoucherTypeEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import cn.iocoder.yudao.module.fms.util.FmsPeriodUtils; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import lombok.Data; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sumBigDecimal; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; + +/** + * FMS 结账方案 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsClosingSchemeServiceImpl implements FmsClosingSchemeService { + + private static final String PRESET_RESOURCE = "fms/closing/closing-scheme-presets.json"; + + @Resource + private FmsClosingSchemeMapper closingSchemeMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsClosingVoucherService closingVoucherService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsFinanceParameterService financeParameterService; + @Resource + private FmsLedgerService ledgerService; + @Resource + private FmsVoucherWordService voucherWordService; + @Resource + private FmsIncomeStatementService incomeStatementService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void initializeDefaultClosingSchemes(Long accountSetId, Long userId) { + // 1.1 加载并校验预置方案 + List presets = loadSchemePresets(); + validateSchemePresets(presets); + // 1.2 查询尚未初始化的预置结账类型 + Set existingTypes = convertSet( + closingSchemeMapper.selectListByAccountSetId(accountSetId), FmsClosingSchemeDO::getType); + List pendingPresets = filterList(presets, preset -> !existingTypes.contains( + FmsClosingTypeEnum.valueOf(preset.getClosingType()).getType())); + if (CollUtil.isEmpty(pendingPresets)) { + return; + } + + // 2. 查询默认凭证字和预置科目 + List voucherWords = voucherWordService.getVoucherWordList(accountSetId); + FmsVoucherWordDO voucherWord = CollUtil.findOne(voucherWords, + item -> Boolean.TRUE.equals(item.getDefaultStatus())); + if (voucherWord == null) { + voucherWord = CollUtil.getFirst(voucherWords); + } + if (voucherWord == null) { + throw exception(CLOSING_SPECIAL_SETTINGS_INVALID); + } + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Map subjectMap = buildPresetSubjectMap(accountSetId, subjects); + + // 3. 创建预置结账方案 + Long voucherWordId = voucherWord.getId(); + closingSchemeMapper.insertBatch(convertList(pendingPresets, + preset -> buildPresetClosingScheme(accountSetId, voucherWordId, preset, subjectMap))); + } + + @Override + public FmsClosingSchemeDO getClosingSchemeByAccountSetIdAndType(Long accountSetId, Integer type) { + return closingSchemeMapper.selectByAccountSetIdAndType(accountSetId, type); + } + + @Override + public FmsClosingSchemeDO validateClosingSchemeExists(Long accountSetId, Long id) { + FmsClosingSchemeDO closing = closingSchemeMapper.selectByIdAndAccountSetId(id, accountSetId); + if (closing == null) { + throw exception(CLOSING_SCHEME_NOT_EXISTS); + } + return closing; + } + + @Override + public Long getClosingSchemeCountBySubjectIds(Long accountSetId, Collection subjectIds) { + Set subjectIdSet = new HashSet<>(subjectIds); + List closings = closingSchemeMapper.selectListByAccountSetId(accountSetId); + // 来源科目、结转科目或科目规则中任一引用命中即计入 + return (long) CollUtil.count(closings, closing -> subjectIdSet.contains(closing.getSubjectId()) + || subjectIdSet.contains(closing.getPriorYearAdjustmentSubjectId()) + || subjectIdSet.contains(closing.getAdjustmentClosingSubjectId()) + || subjectIdSet.contains(closing.getOtherClosingSubjectId()) + || CollUtil.isNotEmpty(closing.getSubjectRules()) && closing.getSubjectRules().stream() + .map(FmsClosingSchemeDO.SubjectRule::getSubjectId) + .anyMatch(subjectIdSet::contains)); + } + + @Override + public Long getClosingSchemeCountByVoucherWordId(Long accountSetId, Long voucherWordId) { + return closingSchemeMapper.selectCountByAccountSetIdAndVoucherWordId(accountSetId, voucherWordId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_SETTINGS_UPDATE_SUB_TYPE, + bizNo = "{{#saveReqVO.accountSetId}}", success = FMS_CLOSING_SETTINGS_UPDATE_SUCCESS) + public Long saveProfitLossSettings(FmsProfitLossSettingsSaveReqVO saveReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(saveReqVO.getAccountSetId(), userId); + // 1.2 校验凭证字和科目 + voucherWordService.validateVoucherWordExists(saveReqVO.getAccountSetId(), saveReqVO.getVoucherWordId()); + validateProfitLossSettings(saveReqVO, userId); + + // 2. 创建或更新结转损益设置 + FmsClosingSchemeDO closing = closingSchemeMapper.selectByAccountSetIdAndType( + saveReqVO.getAccountSetId(), FmsClosingTypeEnum.PROFIT_LOSS.getType()); + FmsClosingSchemeDO updateObj = BeanUtils.toBean(saveReqVO, FmsClosingSchemeDO.class) + .setName(FmsClosingTypeEnum.PROFIT_LOSS.getName()).setPeriodEnd(true) + .setType(FmsClosingTypeEnum.PROFIT_LOSS.getType()); + if (closing == null) { + closingSchemeMapper.insert(updateObj); + return updateObj.getId(); + } + closingSchemeMapper.updateById(updateObj.setId(closing.getId())); + return closing.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getClosingSchemeList( + FmsClosingQueryReqVO queryReqVO, Long userId) { + // 1. 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission( + queryReqVO.getAccountSetId(), userId); + + // 2. 查询方案、余额和已生成凭证 + YearMonth month = LocalDateTimeUtils.parseYearMonth(queryReqVO.getMonth()); + List closings = closingSchemeMapper.selectPeriodEndListByAccountSetId( + queryReqVO.getAccountSetId()); + if (CollUtil.isEmpty(closings)) { + return Collections.emptyList(); + } + Map balanceMap = getSubjectBalanceMap( + queryReqVO.getAccountSetId(), month, userId); + Map yearBalanceMap = closings.stream() + .anyMatch(item -> ObjUtil.equal(item.getTimeType(), + FmsClosingTimeTypeEnum.YEAR_BEGIN.getType())) + ? getSubjectBalanceMap(queryReqVO.getAccountSetId(), + FmsPeriodUtils.getYearStartMonth(accountSet, month), month, userId) : balanceMap; + Map> closingVoucherMap = new HashMap<>(); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + closingVoucherService.getClosingVoucherListByPeriod(queryReqVO.getAccountSetId(), + monthBeginTime, LocalDateTimeUtils.endOfMonth(monthBeginTime)) + .forEach(item -> closingVoucherMap.computeIfAbsent( + item.getClosingId(), key -> new ArrayList<>()).add(item)); + + // 查询账套科目,供专用结转方案按预置科目编码取数 + List subjects = subjectService.getSubjectList( + queryReqVO.getAccountSetId(), null, userId); + + // 3. 组装方案结转金额和凭证编号 + List results = new ArrayList<>(); + for (FmsClosingSchemeDO closing : closings) { + BigDecimal balance; + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.PROFIT_LOSS.getType())) { + balance = calculateProfitLossBalance(queryReqVO.getAccountSetId(), month, userId); + } else if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.REGULAR.getType())) { + balance = calculateClosingSchemeBalance(closing, + ObjUtil.equal(closing.getTimeType(), FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()) + ? yearBalanceMap : balanceMap); + } else { + balance = calculateSpecialClosingBalance(closing, queryReqVO.getAccountSetId(), + month, subjects, balanceMap, userId); + } + List closingVouchers = closingVoucherMap.getOrDefault( + closing.getId(), Collections.emptyList()); + BigDecimal displayBalance = CollUtil.isNotEmpty(closingVouchers) + ? sumBigDecimal(closingVouchers, + item -> NumberUtils.zeroIfNull(item.getAmount())) : balance; + List subjectRules = CollUtil.isEmpty(closing.getSubjectRules()) + ? Collections.emptyList() : BeanUtils.toBean( + closing.getSubjectRules(), FmsClosingSchemeRespVO.SubjectRule.class); + FmsClosingSchemeRespVO respVO = BeanUtils.toBean(closing, FmsClosingSchemeRespVO.class) + .setSubjects(subjectRules) + .setBalance(displayBalance) + .setVoucherIds(closingVouchers.stream().map(FmsClosingVoucherDO::getVoucherId) + .collect(Collectors.toList())); + results.add(respVO); + } + return results; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_SCHEME_CREATE_SUB_TYPE, + bizNo = "{{#createReqVO.accountSetId}}", success = FMS_CLOSING_SCHEME_CREATE_SUCCESS) + public Long createClosingScheme(FmsClosingSchemeSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验凭证字和科目规则 + voucherWordService.validateVoucherWordExists(createReqVO.getAccountSetId(), createReqVO.getVoucherWordId()); + List subjects = validateClosingScheme(createReqVO, userId); + + // 2. 创建结账方案 + FmsClosingSchemeDO closing = BeanUtils.toBean(createReqVO, FmsClosingSchemeDO.class) + .setId(null).setType(FmsClosingTypeEnum.REGULAR.getType()) + .setDigest(createReqVO.getName()) + .setSubjectRules(buildSubjectRules(createReqVO.getSubjects(), subjects)); + closingSchemeMapper.insert(closing); + return closing.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_SCHEME_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.accountSetId}}", success = FMS_CLOSING_SCHEME_UPDATE_SUCCESS) + public void updateClosingScheme(FmsClosingSchemeSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验结账方案 + FmsClosingSchemeDO closing = validateClosingSchemeExists( + updateReqVO.getAccountSetId(), updateReqVO.getId()); + if (!ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.REGULAR.getType())) { + throw exception(CLOSING_SCHEME_NOT_EXISTS); + } + // 1.3 校验凭证字和科目规则 + voucherWordService.validateVoucherWordExists(updateReqVO.getAccountSetId(), updateReqVO.getVoucherWordId()); + List subjects = validateClosingScheme(updateReqVO, userId); + + // 2. 更新结账方案 + closingSchemeMapper.updateById(BeanUtils.toBean(updateReqVO, FmsClosingSchemeDO.class) + .setType(FmsClosingTypeEnum.REGULAR.getType()).setDigest(updateReqVO.getName()) + .setSubjectRules(buildSubjectRules(updateReqVO.getSubjects(), subjects))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_SETTINGS_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.accountSetId}}", success = FMS_CLOSING_SPECIAL_SETTINGS_UPDATE_SUCCESS) + public void updateSpecialClosingSettings( + FmsSpecialClosingSettingsSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验专用结转方案 + FmsClosingSchemeDO closing = validateClosingSchemeExists( + updateReqVO.getAccountSetId(), updateReqVO.getId()); + if (!isSpecialClosing(closing)) { + throw exception(CLOSING_SCHEME_NOT_EXISTS); + } + // 1.3 校验凭证字和科目规则 + voucherWordService.validateVoucherWordExists( + updateReqVO.getAccountSetId(), updateReqVO.getVoucherWordId()); + List subjects = validateSpecialClosingSettings( + closing.getType(), updateReqVO, userId); + + // 2. 更新专用结转设置 + closingSchemeMapper.updateById(new FmsClosingSchemeDO().setId(closing.getId()) + .setVoucherWordId(updateReqVO.getVoucherWordId()) + .setSubjectRules(buildSpecialSubjectRules(updateReqVO.getSubjects(), subjects))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_SCHEME_DELETE_SUB_TYPE, + bizNo = "{{#accountSetId}}", success = FMS_CLOSING_SCHEME_DELETE_SUCCESS) + public void deleteClosingScheme(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验结账方案 + FmsClosingSchemeDO closing = validateClosingSchemeExists(accountSetId, id); + if (!ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.REGULAR.getType())) { + throw exception(CLOSING_SCHEME_NOT_EXISTS); + } + if (closingVoucherService.getClosingVoucherCountByClosingId(id) > 0) { + throw exception(CLOSING_SCHEME_IN_USE); + } + + // 2. 删除结账方案 + closingSchemeMapper.deleteById(id); + } + + /** + * 计算结转损益余额:末级损益科目本期借贷差额绝对值之和 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + * @return 结转损益余额 + */ + private BigDecimal calculateProfitLossBalance(Long accountSetId, YearMonth month, Long userId) { + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId, Objects::nonNull); + Map balanceMap = getSubjectBalanceMap(accountSetId, month, userId); + BigDecimal result = BigDecimal.ZERO; + for (FmsSubjectDO subject : subjects) { + if (ObjUtil.notEqual(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || parentIds.contains(subject.getId())) { + continue; + } + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(subject.getId()); + if (balance != null) { + result = result.add(NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()) + .subtract(NumberUtils.zeroIfNull(balance.getPeriodCreditAmount())).abs()); + } + } + return result; + } + + /** + * 构造预置结账方案 + * + * @param accountSetId 账套编号 + * @param voucherWordId 默认凭证字编号 + * @param preset 预置方案 + * @param subjectMap 标准科目编码到实际科目的 Map + * @return 结账方案 + */ + private FmsClosingSchemeDO buildPresetClosingScheme(Long accountSetId, Long voucherWordId, + SchemePreset preset, Map subjectMap) { + // 1. 构造结账方案公共字段 + FmsClosingTypeEnum closingType = FmsClosingTypeEnum.valueOf(preset.getClosingType()); + FmsClosingSchemeDO scheme = new FmsClosingSchemeDO().setName(preset.getName()) + .setPeriodEnd(preset.getPeriodEnd()).setVoucherWordId(voucherWordId) + .setDigest(preset.getDigest()).setType(closingType.getType()) + .setAccountSetId(accountSetId).setClosingDay(preset.getClosingDay()); + if (closingType == FmsClosingTypeEnum.PROFIT_LOSS) { + // 2. 构造结转损益专用字段 + return scheme.setVoucherType(FmsClosingVoucherTypeEnum.valueOf(preset.getVoucherType()).getType()) + .setReverseBalance(preset.getReverseBalance()) + .setPriorYearAdjustmentSubjectId(getPresetSubject(subjectMap, preset, preset.getPriorYearAdjustmentSubject()).getId()) + .setAdjustmentClosingSubjectId(getPresetSubject(subjectMap, preset, preset.getAdjustmentClosingSubject()).getId()) + .setOtherClosingSubjectId(getPresetSubject(subjectMap, preset, preset.getOtherClosingSubject()).getId()); + } + // 3. 构造其他专用结转方案的科目规则 + preset.getCalculationSubjects().forEach(subject -> getPresetSubject(subjectMap, preset, subject)); + return scheme.setSubjectRules(convertList(preset.getEntries(), entry -> { + FmsSubjectDO subject = getPresetSubject(subjectMap, preset, entry.getSubject()); + return FmsClosingSchemeDO.SubjectRule.builder().subjectId(subject.getId()) + .subjectCode(subject.getCode()).digest(entry.getDigest()) + .direction(FmsDebitCreditDirectionEnum.valueOf(entry.getDirection()).getType()) + .amountRatio(entry.getAmountRatio()).build(); + })); + } + + /** + * 获得预置方案使用的账套科目 + * + * @param subjectMap 标准科目编码到实际科目的 Map + * @param preset 预置方案 + * @param subjectName 预置科目枚举名 + * @return 账套科目 + */ + private FmsSubjectDO getPresetSubject(Map subjectMap, + SchemePreset preset, String subjectName) { + FmsClosingPresetSubjectEnum presetSubject = FmsClosingPresetSubjectEnum.valueOf(subjectName); + FmsSubjectDO subject = subjectMap.get(presetSubject.getCode()); + if (subject == null) { + throw exception(CLOSING_SCHEME_PRESET_SUBJECT_NOT_EXISTS, + preset.getCode(), presetSubject.getCode()); + } + return subject; + } + + /** + * 加载结账方案预置文件 + * + * @return 预置文件 + */ + private List loadSchemePresets() { + try (InputStream inputStream = new ClassPathResource(PRESET_RESOURCE).getInputStream()) { + return JsonUtils.parseArray(IoUtil.readUtf8(inputStream), SchemePreset.class); + } catch (Exception ex) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + } + + /** + * 校验结账方案预置文件 + * + * @param presets 预置方案数组 + */ + private void validateSchemePresets(List presets) { + // 1. 校验预置编码和结账类型唯一 + if (CollUtil.isEmpty(presets) + || convertSet(presets, SchemePreset::getCode).size() != presets.size() + || convertSet(presets, SchemePreset::getClosingType).size() != presets.size()) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + // 2. 校验各类型必需的枚举字段和科目规则 + try { + for (SchemePreset preset : presets) { + FmsClosingTypeEnum closingType = FmsClosingTypeEnum.valueOf(preset.getClosingType()); + if (closingType == FmsClosingTypeEnum.PROFIT_LOSS) { + FmsClosingVoucherTypeEnum.valueOf(preset.getVoucherType()); + FmsClosingPresetSubjectEnum.valueOf(preset.getPriorYearAdjustmentSubject()); + FmsClosingPresetSubjectEnum.valueOf(preset.getAdjustmentClosingSubject()); + FmsClosingPresetSubjectEnum.valueOf(preset.getOtherClosingSubject()); + continue; + } + if (CollUtil.size(preset.getEntries()) < 2) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + for (String subject : preset.getCalculationSubjects()) { + FmsClosingPresetSubjectEnum.valueOf(subject); + } + for (SchemePreset.Entry entry : preset.getEntries()) { + FmsClosingPresetSubjectEnum.valueOf(entry.getSubject()); + FmsDebitCreditDirectionEnum.valueOf(entry.getDirection()); + } + BigDecimal debitRatio = sumBigDecimal(filterList(preset.getEntries(), entry -> Objects.equals( + entry.getDirection(), FmsDebitCreditDirectionEnum.DEBIT.name())), + SchemePreset.Entry::getAmountRatio); + BigDecimal creditRatio = sumBigDecimal(filterList(preset.getEntries(), entry -> Objects.equals( + entry.getDirection(), FmsDebitCreditDirectionEnum.CREDIT.name())), + SchemePreset.Entry::getAmountRatio); + if (debitRatio.compareTo(creditRatio) != 0) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + } + } catch (IllegalArgumentException | NullPointerException ex) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + } + + /** + * 计算专用结转方案的结转金额 + * + * 转出未交增值税取应交增值税期末贷方净额;计提地税按应交增值税和消费税合计乘以比例; + * 计提所得税按利润总额减已计提所得税后乘以比例 + * + * @param closing 结账方案 + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param subjects 账套下的科目列表 + * @param balanceMap 科目编号到余额的 Map + * @param userId 用户编号 + * @return 结转金额 + */ + private BigDecimal calculateSpecialClosingBalance(FmsClosingSchemeDO closing, Long accountSetId, + YearMonth month, List subjects, + Map balanceMap, Long userId) { + Map subjectMap = buildPresetSubjectMap(accountSetId, subjects); + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.UNPAID_VAT.getType())) { + return getPositiveCreditEndingBalance(subjectMap.get(FmsClosingPresetSubjectEnum.VAT_PAYABLE.getCode()), balanceMap); + } + BigDecimal rate = getDirectionRatio( + closing.getSubjectRules(), FmsDebitCreditDirectionEnum.DEBIT.getType()); + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.LOCAL_TAX.getType())) { + BigDecimal taxBase = getPositiveCreditEndingBalance( + subjectMap.get(FmsClosingPresetSubjectEnum.VAT_PAYABLE.getCode()), balanceMap) + .add(getPositiveCreditEndingBalance( + subjectMap.get(FmsClosingPresetSubjectEnum.CONSUMPTION_TAX.getCode()), balanceMap)); + return taxBase.multiply(rate).divide(NumberUtils.ONE_HUNDRED, 2, RoundingMode.HALF_UP); + } + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.INCOME_TAX.getType())) { + FmsReportListReqVO listReqVO = new FmsReportListReqVO(); + listReqVO.setAccountSetId(accountSetId); + listReqVO.setStartMonth(month.toString()); + listReqVO.setEndMonth(month.toString()); + List reportItems = incomeStatementService.getIncomeStatement(listReqVO, userId); + Map reportItemMap = convertMap( + filterList(reportItems, item -> item.getRowNo() != null), + FmsReportItemRespVO::getRowNo, Function.identity(), (first, second) -> first); + BigDecimal totalProfit = reportItemMap.containsKey(30) + ? NumberUtils.zeroIfNull(reportItemMap.get(30).getYearAmount()) : BigDecimal.ZERO; + BigDecimal accruedIncomeTax = reportItemMap.containsKey(31) + ? NumberUtils.zeroIfNull(reportItemMap.get(31).getYearAmount()) : BigDecimal.ZERO; + return totalProfit.max(BigDecimal.ZERO).multiply(rate) + .divide(NumberUtils.ONE_HUNDRED, 2, RoundingMode.HALF_UP) + .subtract(accruedIncomeTax.max(BigDecimal.ZERO)).max(BigDecimal.ZERO); + } + return BigDecimal.ZERO; + } + + /** + * 获得科目期末贷方净额,借方大于贷方时按零处理 + * + * @param subject 科目 + * @param balanceMap 科目编号到余额的 Map + * @return 期末贷方净额 + */ + private BigDecimal getPositiveCreditEndingBalance(FmsSubjectDO subject, + Map balanceMap) { + // 预置科目不存在时直接抛错,避免按错误科目结转 + if (subject == null) { + throw exception(CLOSING_SPECIAL_SUBJECT_NOT_EXISTS); + } + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(subject.getId()); + return balance == null ? BigDecimal.ZERO : NumberUtils.zeroIfNull(balance.getEndingCreditAmount()) + .subtract(NumberUtils.zeroIfNull(balance.getEndingDebitAmount())).max(BigDecimal.ZERO); + } + + /** + * 按标准科目编码构建预置科目 Map + * + * @param accountSetId 账套编号 + * @param subjects 账套科目列表 + * @return 标准科目编码到实际科目的 Map + */ + private Map buildPresetSubjectMap(Long accountSetId, List subjects) { + // 1. 按账套实际科目编码建立索引 + Map actualSubjectMap = convertMap(subjects, FmsSubjectDO::getCode, + Function.identity(), (first, second) -> first); + + // 2. 将各预置标准编码转换为账套实际编码 + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(accountSetId); + Map result = new LinkedHashMap<>(); + for (FmsClosingPresetSubjectEnum presetSubject : FmsClosingPresetSubjectEnum.values()) { + String actualCode = financeParameterService.convertStandardSubjectCode( + presetSubject.getCode(), financeParameter.getSubjectCodeRule()); + FmsSubjectDO subject = actualSubjectMap.get(actualCode); + if (subject != null) { + result.put(presetSubject.getCode(), subject); + } + } + return result; + } + + /** + * 校验常规结账方案的来源科目、科目规则和金额比例,返回账套科目列表 + * + * @param saveReqVO 方案信息 + * @param userId 用户编号 + * @return 账套下的科目列表 + */ + private List validateClosingScheme( + FmsClosingSchemeSaveReqVO saveReqVO, Long userId) { + // 1. 校验来源科目和规则科目存在且为末级科目 + List subjects = subjectService.getSubjectList( + saveReqVO.getAccountSetId(), null, userId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId, Objects::nonNull); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + if (!subjectMap.containsKey(saveReqVO.getSubjectId()) + || parentIds.contains(saveReqVO.getSubjectId())) { + throw exception(CLOSING_SCHEME_SUBJECT_INVALID); + } + for (FmsClosingSchemeSaveReqVO.SubjectRule subjectRule : saveReqVO.getSubjects()) { + if (!subjectMap.containsKey(subjectRule.getSubjectId()) + || parentIds.contains(subjectRule.getSubjectId())) { + throw exception(CLOSING_SCHEME_SUBJECT_INVALID); + } + } + // 2. 校验借方和贷方金额比例分别等于 100% + BigDecimal debitRatio = sumBigDecimal(filterList(saveReqVO.getSubjects(), + item -> ObjUtil.equal(item.getDirection(), FmsDebitCreditDirectionEnum.DEBIT.getType())), + FmsClosingSchemeSaveReqVO.SubjectRule::getAmountRatio); + BigDecimal creditRatio = sumBigDecimal(filterList(saveReqVO.getSubjects(), + item -> ObjUtil.equal(item.getDirection(), FmsDebitCreditDirectionEnum.CREDIT.getType())), + FmsClosingSchemeSaveReqVO.SubjectRule::getAmountRatio); + if (debitRatio.compareTo(NumberUtils.ONE_HUNDRED) != 0 + || creditRatio.compareTo(NumberUtils.ONE_HUNDRED) != 0) { + throw exception(CLOSING_SCHEME_RATIO_INVALID); + } + return subjects; + } + + /** + * 构造常规结账方案的科目规则,按科目编号补充科目编码快照 + * + * @param saveRules 保存的科目规则 + * @param subjects 账套下的科目列表 + * @return 科目规则 + */ + private List buildSubjectRules( + List saveRules, List subjects) { + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + List rules = BeanUtils.toBean(saveRules, FmsClosingSchemeDO.SubjectRule.class); + rules.forEach(rule -> rule.setSubjectCode(subjectMap.get(rule.getSubjectId()).getCode())); + return rules; + } + + /** + * 校验专用结转的科目规则和金额比例,返回账套科目列表 + * + * 借方比例必须等于贷方比例且不超过 100%;转出未交增值税的借方比例必须等于 100% + * + * @param closingType 结转类型 + * @param saveReqVO 设置信息 + * @param userId 用户编号 + * @return 账套下的科目列表 + */ + private List validateSpecialClosingSettings(Integer closingType, + FmsSpecialClosingSettingsSaveReqVO saveReqVO, Long userId) { + // 1. 校验规则科目存在且为末级科目 + List subjects = subjectService.getSubjectList( + saveReqVO.getAccountSetId(), null, userId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId, Objects::nonNull); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + if (saveReqVO.getSubjects().stream().anyMatch(subjectRule -> + !subjectMap.containsKey(subjectRule.getSubjectId()) + || parentIds.contains(subjectRule.getSubjectId()))) { + throw exception(CLOSING_SPECIAL_SETTINGS_INVALID); + } + // 2. 校验借贷比例相等且不超过 100% + BigDecimal debitRatio = sumBigDecimal(filterList(saveReqVO.getSubjects(), + item -> ObjUtil.equal(item.getDirection(), FmsDebitCreditDirectionEnum.DEBIT.getType())), + FmsSpecialClosingSettingsSaveReqVO.SubjectRule::getAmountRatio); + BigDecimal creditRatio = sumBigDecimal(filterList(saveReqVO.getSubjects(), + item -> ObjUtil.equal(item.getDirection(), FmsDebitCreditDirectionEnum.CREDIT.getType())), + FmsSpecialClosingSettingsSaveReqVO.SubjectRule::getAmountRatio); + boolean unpaidVatRatioInvalid = ObjUtil.equal( + closingType, FmsClosingTypeEnum.UNPAID_VAT.getType()) + && debitRatio.compareTo(NumberUtils.ONE_HUNDRED) != 0; + if (debitRatio.signum() <= 0 || debitRatio.compareTo(creditRatio) != 0 + || debitRatio.compareTo(NumberUtils.ONE_HUNDRED) > 0 || unpaidVatRatioInvalid) { + throw exception(CLOSING_SPECIAL_SETTINGS_INVALID); + } + return subjects; + } + + /** + * 构造专用结转的科目规则,按科目编号补充科目编码快照 + * + * @param saveRules 保存的科目规则 + * @param subjects 账套下的科目列表 + * @return 科目规则 + */ + private List buildSpecialSubjectRules( + List saveRules, + List subjects) { + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + List rules = BeanUtils.toBean( + saveRules, FmsClosingSchemeDO.SubjectRule.class); + rules.forEach(rule -> rule.setSubjectCode( + subjectMap.get(rule.getSubjectId()).getCode())); + return rules; + } + + /** + * 校验结转损益设置的科目合法性 + * + * @param saveReqVO 设置信息 + * @param userId 用户编号 + */ + private void validateProfitLossSettings(FmsProfitLossSettingsSaveReqVO saveReqVO, Long userId) { + List subjects = subjectService.getSubjectList(saveReqVO.getAccountSetId(), null, userId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId, Objects::nonNull); + validateProfitLossSubjects(BeanUtils.toBean(saveReqVO, FmsClosingSchemeDO.class), + subjects, parentIds); + } + + /** + * 校验结转损益的三个科目:调整科目必须为末级损益科目,结转科目必须为末级非损益科目 + * + * @param closing 结账方案 + * @param subjects 账套下的科目列表 + * @param parentIds 父级科目编号集合 + */ + private void validateProfitLossSubjects(FmsClosingSchemeDO closing, + List subjects, Set parentIds) { + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + FmsSubjectDO adjustmentSubject = subjectMap.get(closing.getPriorYearAdjustmentSubjectId()); + FmsSubjectDO adjustmentClosingSubject = subjectMap.get(closing.getAdjustmentClosingSubjectId()); + FmsSubjectDO otherClosingSubject = subjectMap.get(closing.getOtherClosingSubjectId()); + if (adjustmentSubject == null + || !ObjUtil.equal(adjustmentSubject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || parentIds.contains(adjustmentSubject.getId()) + || adjustmentClosingSubject == null + || parentIds.contains(adjustmentClosingSubject.getId()) + || ObjUtil.equal(adjustmentClosingSubject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || otherClosingSubject == null + || parentIds.contains(otherClosingSubject.getId()) + || ObjUtil.equal(otherClosingSubject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType())) { + throw exception(CLOSING_PROFIT_LOSS_SETTINGS_INVALID); + } + } + + /** + * 计算常规结账方案的来源科目结转金额 + * + * 按取数时间类型选择期初、期末、本期或本年累计口径,再按取数规则返回余额或发生额 + * + * @param closing 结账方案 + * @param balanceMap 科目编号到余额的 Map + * @return 结转金额 + */ + private BigDecimal calculateClosingSchemeBalance(FmsClosingSchemeDO closing, + Map balanceMap) { + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(closing.getSubjectId()); + if (balance == null) { + return BigDecimal.ZERO; + } + // 期初、年初或期末余额口径 + boolean openingBalance = ObjectUtils.equalsAny(closing.getTimeType(), + FmsClosingTimeTypeEnum.PERIOD_BEGIN.getType(), FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()); + BigDecimal debitBalance = openingBalance + ? NumberUtils.zeroIfNull(balance.getOpeningDebitAmount()) + : NumberUtils.zeroIfNull(balance.getEndingDebitAmount()); + BigDecimal creditBalance = openingBalance + ? NumberUtils.zeroIfNull(balance.getOpeningCreditAmount()) + : NumberUtils.zeroIfNull(balance.getEndingCreditAmount()); + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.BALANCE.getRule())) { + return debitBalance.subtract(creditBalance).abs(); + } + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + || ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.SUBJECT_DEBIT_BALANCE.getRule())) { + return debitBalance; + } + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.CREDIT_BALANCE.getRule()) + || ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.SUBJECT_CREDIT_BALANCE.getRule())) { + return creditBalance; + } + // 本期或本年累计发生额口径 + BigDecimal debitAmount = ObjUtil.equal(closing.getTimeType(), FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()) + ? NumberUtils.zeroIfNull(balance.getYearDebitAmount()) + : NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()); + BigDecimal creditAmount = ObjUtil.equal(closing.getTimeType(), FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()) + ? NumberUtils.zeroIfNull(balance.getYearCreditAmount()) + : NumberUtils.zeroIfNull(balance.getPeriodCreditAmount()); + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.DEBIT_AMOUNT.getRule())) { + return debitAmount; + } + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.CREDIT_AMOUNT.getRule())) { + return creditAmount; + } + return debitAmount.subtract(creditAmount).abs(); + } + + /** + * 汇总科目规则中指定借贷方向的金额比例 + * + * @param subjectRules 科目规则 + * @param direction 借贷方向 + * @return 金额比例合计 + */ + private BigDecimal getDirectionRatio(List subjectRules, Integer direction) { + return sumBigDecimal(filterList(subjectRules, + item -> ObjUtil.equal(item.getDirection(), direction)), FmsClosingSchemeDO.SubjectRule::getAmountRatio); + } + + /** + * 判断是否为专用结转方案(转出未交增值税、计提地税、计提所得税) + * + * @param closing 结账方案 + * @return 是否专用结转方案 + */ + private boolean isSpecialClosing(FmsClosingSchemeDO closing) { + return ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.UNPAID_VAT.getType()) + || ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.LOCAL_TAX.getType()) + || ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.INCOME_TAX.getType()); + } + + /** + * 获得单个会计期间的科目余额 Map + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + * @return 科目编号到余额的 Map + */ + private Map getSubjectBalanceMap( + Long accountSetId, YearMonth month, Long userId) { + return getSubjectBalanceMap(accountSetId, month, month, userId); + } + + /** + * 获得月份区间的科目余额 Map,树形余额扁平化后按科目编号存放 + * + * @param accountSetId 账套编号 + * @param startMonth 开始月份 + * @param endMonth 结束月份 + * @param userId 用户编号 + * @return 科目编号到余额的 Map + */ + private Map getSubjectBalanceMap( + Long accountSetId, YearMonth startMonth, YearMonth endMonth, Long userId) { + FmsLedgerListReqVO listReqVO = new FmsLedgerListReqVO().setAccountSetId(accountSetId) + .setStartMonth(startMonth.toString()).setEndMonth(endMonth.toString()); + Map result = new LinkedHashMap<>(); + flattenBalances(ledgerService.getSubjectBalanceList(listReqVO, userId), result); + return result; + } + + /** + * 递归扁平化树形科目余额到 Map + * + * @param balances 树形科目余额 + * @param result 科目编号到余额的 Map + */ + private void flattenBalances(List balances, + Map result) { + for (FmsLedgerSubjectBalanceRespVO balance : balances) { + result.put(balance.getSubjectId(), balance); + flattenBalances(balance.getChildren(), result); + } + } + + @Data + private static class SchemePreset { + + /** + * 预置编码 + */ + private String code; + /** + * 方案名称 + */ + private String name; + /** + * 结账类型 + */ + private String closingType; + /** + * 凭证摘要 + */ + private String digest; + /** + * 是否期末结转 + */ + private Boolean periodEnd; + /** + * 结转日期 + */ + private Integer closingDay; + /** + * 结转凭证类型 + */ + private String voucherType; + /** + * 是否按余额反向结转 + */ + private Boolean reverseBalance; + /** + * 以前年度损益调整科目 + */ + private String priorYearAdjustmentSubject; + /** + * 以前年度损益调整结转科目 + */ + private String adjustmentClosingSubject; + /** + * 其他损益结转科目 + */ + private String otherClosingSubject; + /** + * 计算科目数组 + */ + private List calculationSubjects; + /** + * 分录数组 + */ + private List entries; + + @Data + private static class Entry { + + /** + * 预置科目 + */ + private String subject; + /** + * 摘要 + */ + private String digest; + /** + * 借贷方向 + */ + private String direction; + /** + * 金额比例 + */ + private BigDecimal amountRatio; + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateService.java new file mode 100644 index 0000000..27aa22b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateService.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateSaveReqVO; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 结账模板 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsClosingTemplateService { + + /** + * 初始化账套结账模板 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + */ + void initializeClosingTemplates(Long accountSetId, Long userId); + + /** + * 获得结账模板列表 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 结账模板列表 + */ + List getClosingTemplateList(Long accountSetId, Long userId); + + /** + * 获得引用指定科目的结账模板数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号集合 + * @return 结账模板数量 + */ + Long getClosingTemplateCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 创建结账模板 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 模板编号 + */ + Long createClosingTemplate(FmsClosingTemplateSaveReqVO createReqVO, Long userId); + + /** + * 更新结账模板 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateClosingTemplate(FmsClosingTemplateSaveReqVO updateReqVO, Long userId); + + /** + * 删除结账模板 + * + * @param accountSetId 账套编号 + * @param id 模板编号 + * @param userId 用户编号 + */ + void deleteClosingTemplate(Long accountSetId, Long id, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateServiceImpl.java new file mode 100644 index 0000000..3ae9dc4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateServiceImpl.java @@ -0,0 +1,372 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingTemplateMapper; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTemplateCategoryEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import lombok.Data; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sumBigDecimal; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_SCHEME_RATIO_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_SCHEME_SUBJECT_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_TEMPLATE_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_TEMPLATE_PRESET_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_TEMPLATE_PRESET_SUBJECT_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CLOSING_TEMPLATE_CREATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CLOSING_TEMPLATE_CREATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CLOSING_TEMPLATE_DELETE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CLOSING_TEMPLATE_DELETE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CLOSING_TEMPLATE_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CLOSING_TEMPLATE_UPDATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CLOSING_TYPE; + +/** + * FMS 结账模板 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsClosingTemplateServiceImpl implements FmsClosingTemplateService { + + private static final String PRESET_RESOURCE = "fms/closing/closing-template-presets.json"; + + @Resource + private FmsClosingTemplateMapper closingTemplateMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsFinanceParameterService financeParameterService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void initializeClosingTemplates(Long accountSetId, Long userId) { + // 1.1 加载并校验预置模板 + List presets = loadPresets(); + validatePresets(presets); + // 1.2 查询尚未初始化的预置编码 + Set existingCodes = convertSet(closingTemplateMapper.selectListByAccountSetId(accountSetId), + FmsClosingTemplateDO::getPresetCode, Objects::nonNull); + List pendingPresets = filterList(presets, + preset -> !existingCodes.contains(preset.getCode())); + if (CollUtil.isEmpty(pendingPresets)) { + return; + } + + // 2. 查询账套科目并建立实际科目编码索引 + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode, + Function.identity(), (first, second) -> first); + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(accountSetId); + + // 3. 转换预置模板并批量创建 + List templates = convertList(pendingPresets, + preset -> buildPresetTemplate(accountSetId, preset, subjectMap, + financeParameter.getSubjectCodeRule())); + closingTemplateMapper.insertBatch(templates); + } + + @Override + public List getClosingTemplateList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询结账模板 + return convertList(closingTemplateMapper.selectListByAccountSetId(accountSetId), this::buildTemplateRespVO); + } + + @Override + public Long getClosingTemplateCountBySubjectIds(Long accountSetId, Collection subjectIds) { + // 1. 查询账套结账模板 + List templates = closingTemplateMapper.selectListByAccountSetId(accountSetId); + + // 2. 统计来源科目或分录科目被引用的模板数量 + Set subjectIdSet = new HashSet<>(subjectIds); + return (long) filterList(templates, template -> subjectIdSet.contains(template.getSubjectId()) + || CollUtil.emptyIfNull(template.getSubjectRules()).stream() + .anyMatch(rule -> subjectIdSet.contains(rule.getSubjectId()))).size(); + } + + @Override + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_TEMPLATE_CREATE_SUB_TYPE, + bizNo = "{{#templateId}}", success = FMS_CLOSING_TEMPLATE_CREATE_SUCCESS) + public Long createClosingTemplate(FmsClosingTemplateSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验模板科目和金额比例 + Map subjectMap = validateClosingTemplate(createReqVO, userId); + + // 2. 创建结账模板 + FmsClosingTemplateDO template = buildClosingTemplate(createReqVO, subjectMap).setId(null); + closingTemplateMapper.insert(template); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("templateId", template.getId()); + return template.getId(); + } + + @Override + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_TEMPLATE_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_CLOSING_TEMPLATE_UPDATE_SUCCESS) + public void updateClosingTemplate(FmsClosingTemplateSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验结账模板存在 + validateClosingTemplateExists(updateReqVO.getAccountSetId(), updateReqVO.getId()); + // 1.3 校验模板科目和金额比例 + Map subjectMap = validateClosingTemplate(updateReqVO, userId); + + // 2. 更新结账模板 + closingTemplateMapper.updateById(buildClosingTemplate(updateReqVO, subjectMap)); + } + + @Override + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_TEMPLATE_DELETE_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_CLOSING_TEMPLATE_DELETE_SUCCESS) + public void deleteClosingTemplate(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验结账模板存在 + validateClosingTemplateExists(accountSetId, id); + + // 2. 删除结账模板 + closingTemplateMapper.deleteById(id); + } + + /** + * 校验结账模板存在 + * + * @param accountSetId 账套编号 + * @param id 模板编号 + * @return 结账模板 + */ + private FmsClosingTemplateDO validateClosingTemplateExists(Long accountSetId, Long id) { + FmsClosingTemplateDO template = closingTemplateMapper.selectByIdAndAccountSetId(id, accountSetId); + if (template == null) { + throw exception(CLOSING_TEMPLATE_NOT_EXISTS); + } + return template; + } + + /** + * 校验模板科目和金额比例 + * + * @param saveReqVO 模板信息 + * @param userId 用户编号 + * @return 科目编号到科目的 Map + */ + private Map validateClosingTemplate( + FmsClosingTemplateSaveReqVO saveReqVO, Long userId) { + // 1. 校验来源科目和规则科目存在且为末级科目 + List subjects = subjectService.getSubjectList(saveReqVO.getAccountSetId(), null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId, Objects::nonNull); + if (saveReqVO.getSubjectId() != null + && (!subjectMap.containsKey(saveReqVO.getSubjectId()) || parentIds.contains(saveReqVO.getSubjectId()))) { + throw exception(CLOSING_SCHEME_SUBJECT_INVALID); + } + for (FmsClosingTemplateSaveReqVO.SubjectRule rule : saveReqVO.getSubjects()) { + if (!subjectMap.containsKey(rule.getSubjectId()) || parentIds.contains(rule.getSubjectId())) { + throw exception(CLOSING_SCHEME_SUBJECT_INVALID); + } + } + + // 2. 校验借贷金额比例分别等于 100% + BigDecimal debitRatio = sumBigDecimal(filterList(saveReqVO.getSubjects(), rule -> Objects.equals( + rule.getDirection(), FmsDebitCreditDirectionEnum.DEBIT.getType())), + FmsClosingTemplateSaveReqVO.SubjectRule::getAmountRatio); + BigDecimal creditRatio = sumBigDecimal(filterList(saveReqVO.getSubjects(), rule -> Objects.equals( + rule.getDirection(), FmsDebitCreditDirectionEnum.CREDIT.getType())), + FmsClosingTemplateSaveReqVO.SubjectRule::getAmountRatio); + if (debitRatio.compareTo(BigDecimal.valueOf(100)) != 0 + || creditRatio.compareTo(BigDecimal.valueOf(100)) != 0) { + throw exception(CLOSING_SCHEME_RATIO_INVALID); + } + return subjectMap; + } + + /** + * 构造结账模板 + * + * @param saveReqVO 模板信息 + * @param subjectMap 科目编号到科目的 Map + * @return 结账模板 + */ + private FmsClosingTemplateDO buildClosingTemplate(FmsClosingTemplateSaveReqVO saveReqVO, + Map subjectMap) { + List rules = BeanUtils.toBean( + saveReqVO.getSubjects(), FmsClosingTemplateDO.SubjectRule.class); + rules.forEach(rule -> rule.setSubjectCode(subjectMap.get(rule.getSubjectId()).getCode())); + return BeanUtils.toBean(saveReqVO, FmsClosingTemplateDO.class).setSubjectRules(rules); + } + + /** + * 构造结账模板响应 + * + * @param template 结账模板 + * @return 结账模板响应 + */ + private FmsClosingTemplateRespVO buildTemplateRespVO(FmsClosingTemplateDO template) { + return BeanUtils.toBean(template, FmsClosingTemplateRespVO.class) + .setSubjects(BeanUtils.toBean(template.getSubjectRules(), FmsClosingTemplateRespVO.SubjectRule.class)); + } + + /** + * 构造预置结账模板 + * + * @param accountSetId 账套编号 + * @param preset 预置模板 + * @param subjectMap 实际科目编码到科目的 Map + * @param subjectCodeRule 科目编码规则 + * @return 结账模板 + */ + private FmsClosingTemplateDO buildPresetTemplate(Long accountSetId, Preset preset, + Map subjectMap, String subjectCodeRule) { + List rules = convertList(preset.getEntries(), entry -> { + String actualCode = financeParameterService.convertStandardSubjectCode( + entry.getSubjectCode(), subjectCodeRule); + FmsSubjectDO subject = subjectMap.get(actualCode); + if (subject == null) { + throw exception(CLOSING_TEMPLATE_PRESET_SUBJECT_NOT_EXISTS, preset.getCode(), entry.getSubjectCode()); + } + return FmsClosingTemplateDO.SubjectRule.builder().subjectId(subject.getId()) + .subjectCode(subject.getCode()).digest(entry.getDigest()) + .direction(FmsDebitCreditDirectionEnum.valueOf(entry.getDirection()).getType()) + .amountRatio(entry.getAmountRatio()).build(); + }); + return new FmsClosingTemplateDO().setAccountSetId(accountSetId).setPresetCode(preset.getCode()) + .setName(preset.getName()).setCategory(FmsClosingTemplateCategoryEnum.valueOf(preset.getCategory()).getCategory()) + .setPeriodEnd(true).setSubjectRules(rules).setSort(preset.getSort()); + } + + /** + * 加载结账模板预置文件 + * + * @return 预置文件 + */ + private List loadPresets() { + try (InputStream inputStream = new ClassPathResource(PRESET_RESOURCE).getInputStream()) { + return JsonUtils.parseArray(IoUtil.readUtf8(inputStream), Preset.class); + } catch (Exception ex) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + } + + /** + * 校验结账模板预置文件 + * + * @param presets 预置模板数组 + */ + private void validatePresets(List presets) { + // 1. 校验预置编码唯一 + if (CollUtil.isEmpty(presets) || convertSet(presets, Preset::getCode).size() != presets.size()) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + for (Preset preset : presets) { + // 2. 校验模板分类、分录数量和枚举字段 + try { + FmsClosingTemplateCategoryEnum.valueOf(preset.getCategory()); + if (CollUtil.size(preset.getEntries()) < 2) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + for (Preset.Entry entry : preset.getEntries()) { + FmsDebitCreditDirectionEnum.valueOf(entry.getDirection()); + } + } catch (IllegalArgumentException | NullPointerException ex) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + // 3. 校验借贷金额比例分别等于 100% + BigDecimal debitRatio = sumBigDecimal(filterList(preset.getEntries(), entry -> Objects.equals( + entry.getDirection(), FmsDebitCreditDirectionEnum.DEBIT.name())), Preset.Entry::getAmountRatio); + BigDecimal creditRatio = sumBigDecimal(filterList(preset.getEntries(), entry -> Objects.equals( + entry.getDirection(), FmsDebitCreditDirectionEnum.CREDIT.name())), Preset.Entry::getAmountRatio); + if (debitRatio.compareTo(BigDecimal.valueOf(100)) != 0 + || creditRatio.compareTo(BigDecimal.valueOf(100)) != 0) { + throw exception(CLOSING_TEMPLATE_PRESET_INVALID); + } + } + } + + @Data + private static class Preset { + + /** + * 预置编码 + */ + private String code; + /** + * 模板名称 + */ + private String name; + /** + * 模板分类 + */ + private String category; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 分录数组 + */ + private List entries; + + @Data + private static class Entry { + + /** + * 标准科目编码 + */ + private String subjectCode; + /** + * 摘要 + */ + private String digest; + /** + * 借贷方向 + */ + private String direction; + /** + * 金额比例 + */ + private BigDecimal amountRatio; + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherService.java new file mode 100644 index 0000000..7935955 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherService.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingVoucherGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossGenerateReqVO; + +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * FMS 结转凭证 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsClosingVoucherService { + + /** + * 获得指定期间的结转凭证关联列表 + * + * @param accountSetId 账套编号 + * @param beginTime 期间开始时间 + * @param endTime 期间结束时间 + * @return 结转凭证关联列表 + */ + List getClosingVoucherListByPeriod(Long accountSetId, + LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获得指定方案和期间的结转凭证关联列表 + * + * @param closingId 结账方案编号 + * @param beginTime 开始时间,为空时不限制 + * @param endTime 结束时间,为空时不限制 + * @return 结转凭证关联列表 + */ + List getClosingVoucherListByClosingIdAndPeriod(Long closingId, + LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 更新指定期间的结转凭证结账状态 + * + * @param accountSetId 账套编号 + * @param beginTime 期间开始时间 + * @param endTime 期间结束时间 + * @param closed 是否已结账 + */ + void updateClosingVoucherClosedByPeriod(Long accountSetId, LocalDateTime beginTime, + LocalDateTime endTime, Boolean closed); + + /** + * 获得方案已生成的结转凭证数量 + * + * @param closingId 方案编号 + * @return 结转凭证数量 + */ + Long getClosingVoucherCountByClosingId(Long closingId); + + /** + * 获得结转凭证编号集合 + * + * @param accountSetId 账套编号 + * @param voucherIds 凭证编号数组 + * @return 结转凭证编号集合 + */ + Set getClosingVoucherIdSet(Long accountSetId, Collection voucherIds); + + /** + * 生成结转损益凭证 + * + * @param generateReqVO 生成参数 + * @param userId 用户编号 + * @return 凭证编号 + */ + Long generateProfitLossVoucher(FmsProfitLossGenerateReqVO generateReqVO, Long userId); + + /** + * 生成结账方案凭证 + * + * @param generateReqVO 生成参数 + * @param userId 用户编号 + * @return 凭证编号 + */ + Long generateClosingSchemeVoucher(FmsClosingSchemeGenerateReqVO generateReqVO, Long userId); + + /** + * 批量生成结转凭证 + * + * @param generateReqVO 生成参数 + * @param userId 用户编号 + * @return 凭证编号数组 + */ + List generateClosingVoucherList(FmsClosingVoucherGenerateReqVO generateReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherServiceImpl.java new file mode 100644 index 0000000..874c646 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherServiceImpl.java @@ -0,0 +1,804 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingVoucherGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerEntryVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherEntrySaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingSchemeMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingVoucherMapper; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingPresetSubjectEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingVoucherTypeEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import cn.iocoder.yudao.module.fms.util.FmsPeriodUtils; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sumBigDecimal; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; + +/** + * FMS 结转凭证 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsClosingVoucherServiceImpl implements FmsClosingVoucherService { + + @Resource + private FmsClosingVoucherMapper closingVoucherMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + @Lazy // 延迟加载,避免与结账期间 Service 循环依赖 + private FmsClosingPeriodService closingPeriodService; + @Resource + @Lazy // 延迟加载,避免与结账方案 Service 循环依赖 + private FmsClosingSchemeService closingSchemeService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsLedgerService ledgerService; + @Resource + @Lazy // 延迟加载,避免与凭证 Service 循环依赖 + private FmsVoucherService voucherService; + @Resource + private FmsVoucherWordService voucherWordService; + @Resource + private FmsIncomeStatementService incomeStatementService; + + @Override + public List getClosingVoucherListByPeriod(Long accountSetId, + LocalDateTime beginTime, LocalDateTime endTime) { + return closingVoucherMapper.selectListByPeriod(accountSetId, beginTime, endTime); + } + + @Override + public List getClosingVoucherListByClosingIdAndPeriod(Long closingId, + LocalDateTime beginTime, LocalDateTime endTime) { + return closingVoucherMapper.selectListByClosingIdAndVoucherTimeBetween(closingId, beginTime, endTime); + } + + @Override + public void updateClosingVoucherClosedByPeriod(Long accountSetId, LocalDateTime beginTime, + LocalDateTime endTime, Boolean closed) { + List closingVouchers = getClosingVoucherListByPeriod( + accountSetId, beginTime, endTime); + closingVouchers.forEach(item -> closingVoucherMapper.updateById(new FmsClosingVoucherDO() + .setId(item.getId()).setClosed(closed))); + } + + @Override + public Long getClosingVoucherCountByClosingId(Long closingId) { + return closingVoucherMapper.selectCountByClosingId(closingId); + } + + @Override + public Set getClosingVoucherIdSet(Long accountSetId, Collection voucherIds) { + if (CollUtil.isEmpty(voucherIds)) { + return Collections.emptySet(); + } + List closingVouchers = closingVoucherMapper.selectListByAccountSetIdAndVoucherIds( + accountSetId, voucherIds); + return convertSet(closingVouchers, FmsClosingVoucherDO::getVoucherId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_VOUCHER_GENERATE_SUB_TYPE, + bizNo = "{{#generateReqVO.accountSetId}}", success = FMS_CLOSING_PROFIT_LOSS_VOUCHER_GENERATE_SUCCESS) + public Long generateProfitLossVoucher(FmsProfitLossGenerateReqVO generateReqVO, Long userId) { + // 1.1 校验当前会计期间 + validateCurrentPeriod(generateReqVO.getAccountSetId(), generateReqVO.getMonth(), userId); + // 1.2 校验期间未结账 + YearMonth month = LocalDateTimeUtils.parseYearMonth(generateReqVO.getMonth()); + if (closingPeriodService.isPeriodClosed(generateReqVO.getAccountSetId(), generateReqVO.getMonth())) { + throw exception(CLOSING_PERIOD_CLOSED); + } + // 1.3 校验结转损益设置 + FmsClosingSchemeDO closing = closingSchemeService.getClosingSchemeByAccountSetIdAndType( + generateReqVO.getAccountSetId(), FmsClosingTypeEnum.PROFIT_LOSS.getType()); + if (closing == null) { + throw exception(CLOSING_PROFIT_LOSS_SETTINGS_NOT_EXISTS); + } + voucherWordService.validateVoucherWordExists(generateReqVO.getAccountSetId(), closing.getVoucherWordId()); + List existingVouchers = closingVoucherMapper.selectListByClosingIdAndPeriod( + closing.getId(), LocalDateTimeUtils.getMonthBeginTime(month), + LocalDateTimeUtils.endOfMonth(LocalDateTimeUtils.getMonthBeginTime(month))); + deleteExistingClosingVouchers( + generateReqVO.getAccountSetId(), existingVouchers, userId); + List subjects = subjectService.getSubjectList( + generateReqVO.getAccountSetId(), null, userId); + Set parentIds = subjects.stream().map(FmsSubjectDO::getParentId) + .filter(id -> id != null).collect(Collectors.toSet()); + validateProfitLossSubjects(closing, subjects, parentIds); + + // 2. 计算末级损益科目余额并按凭证分类构造分录 + Map balanceMap = getSubjectBalanceMap( + generateReqVO.getAccountSetId(), month, userId); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + List ledgerEntries = ledgerService.getEntryList( + generateReqVO.getAccountSetId(), monthBeginTime, monthBeginTime.plusMonths(1), userId); + List> voucherEntriesList = new ArrayList<>(); + if (ObjUtil.equal(closing.getVoucherType(), + FmsClosingVoucherTypeEnum.SEPARATE_GAIN_AND_LOSS.getType())) { + List gainEntries = buildProfitLossEntries( + closing, subjects, parentIds, balanceMap, ledgerEntries, true); + List lossEntries = buildProfitLossEntries( + closing, subjects, parentIds, balanceMap, ledgerEntries, false); + if (CollUtil.isNotEmpty(gainEntries)) { + voucherEntriesList.add(gainEntries); + } + if (CollUtil.isNotEmpty(lossEntries)) { + voucherEntriesList.add(lossEntries); + } + } else { + List entries = buildProfitLossEntries( + closing, subjects, parentIds, balanceMap, ledgerEntries, null); + if (CollUtil.isNotEmpty(entries)) { + voucherEntriesList.add(entries); + } + } + if (CollUtil.isEmpty(voucherEntriesList)) { + throw exception(CLOSING_NO_PROFIT_LOSS_BALANCE); + } + + // 3. 创建结转凭证并登记关联 + LocalDateTime voucherTime = month.atDay(Math.min(closing.getClosingDay(), month.lengthOfMonth())) + .atStartOfDay(); + Long firstVoucherId = null; + for (List entries : voucherEntriesList) { + FmsVoucherSaveReqVO voucherReqVO = new FmsVoucherSaveReqVO(); + voucherReqVO.setAccountSetId(generateReqVO.getAccountSetId()); + voucherReqVO.setVoucherWordId(closing.getVoucherWordId()); + voucherReqVO.setVoucherTime(voucherTime); + voucherReqVO.setVoucherNumber(voucherService.getNextVoucherNumber( + generateReqVO.getAccountSetId(), closing.getVoucherWordId(), voucherTime, userId)); + voucherReqVO.setEntries(entries); + Long voucherId = voucherService.createVoucher(voucherReqVO, userId); + if (firstVoucherId == null) { + firstVoucherId = voucherId; + } + closingVoucherMapper.insert(new FmsClosingVoucherDO().setClosingId(closing.getId()) + .setVoucherId(voucherId).setVoucherTime(voucherTime) + .setAmount(entries.stream().map(FmsVoucherEntrySaveReqVO::getDebitAmount) + .map(NumberUtils::zeroIfNull).reduce(BigDecimal.ZERO, BigDecimal::add)) + .setClosed(false) + .setAccountSetId(generateReqVO.getAccountSetId())); + } + return firstVoucherId; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_VOUCHER_GENERATE_SUB_TYPE, + bizNo = "{{#generateReqVO.accountSetId}}", success = FMS_CLOSING_SCHEME_VOUCHER_GENERATE_SUCCESS) + public Long generateClosingSchemeVoucher( + FmsClosingSchemeGenerateReqVO generateReqVO, Long userId) { + // 1.1 校验当前会计期间 + FmsAccountSetDO accountSet = validateCurrentPeriod( + generateReqVO.getAccountSetId(), generateReqVO.getMonth(), userId); + // 1.2 校验期间未结账 + if (closingPeriodService.isPeriodClosed(generateReqVO.getAccountSetId(), generateReqVO.getMonth())) { + throw exception(CLOSING_PERIOD_CLOSED); + } + // 1.3 校验结账方案 + FmsClosingSchemeDO closing = closingSchemeService.validateClosingSchemeExists( + generateReqVO.getAccountSetId(), generateReqVO.getId()); + if (!ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.REGULAR.getType()) + && !isSpecialClosing(closing)) { + throw exception(CLOSING_SCHEME_NOT_EXISTS); + } + validatePeriodEndClosing(closing); + voucherWordService.validateVoucherWordExists( + generateReqVO.getAccountSetId(), closing.getVoucherWordId()); + YearMonth month = LocalDateTimeUtils.parseYearMonth(generateReqVO.getMonth()); + + // 2. 重新生成时先删除当前期间的旧凭证和关联 + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + List existingVouchers = closingVoucherMapper.selectListByClosingIdAndPeriod( + closing.getId(), monthBeginTime, LocalDateTimeUtils.endOfMonth(monthBeginTime)); + deleteExistingClosingVouchers(generateReqVO.getAccountSetId(), existingVouchers, userId); + + // 3. 计算结转金额并校验非零 + Map balanceMap = ObjUtil.equal(closing.getTimeType(), FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()) + ? getSubjectBalanceMap(generateReqVO.getAccountSetId(), + FmsPeriodUtils.getYearStartMonth(accountSet, month), month, userId) + : getSubjectBalanceMap(generateReqVO.getAccountSetId(), month, userId); + List subjects = subjectService.getSubjectList(generateReqVO.getAccountSetId(), null, userId); + BigDecimal balance = isSpecialClosing(closing) + ? calculateSpecialClosingBalance(closing, generateReqVO.getAccountSetId(), month, subjects, balanceMap, userId) + : calculateClosingSchemeBalance(closing, balanceMap); + if (balance.signum() == 0) { + throw exception(CLOSING_SCHEME_NO_BALANCE); + } + + // 4. 按 JSON 科目规则构造凭证分录 + List entries = isSpecialClosing(closing) + ? buildSpecialClosingEntries(closing, balance) + : buildRegularClosingEntries(closing, balance); + + // 5. 创建凭证并登记方案关联 + LocalDateTime voucherTime = month.atEndOfMonth().atStartOfDay(); + Long voucherId = voucherService.createVoucher(new FmsVoucherSaveReqVO() + .setAccountSetId(generateReqVO.getAccountSetId()) + .setVoucherWordId(closing.getVoucherWordId()) + .setVoucherTime(voucherTime) + .setVoucherNumber(voucherService.getNextVoucherNumber(generateReqVO.getAccountSetId(), + closing.getVoucherWordId(), voucherTime, userId)) + .setEntries(entries), userId); + closingVoucherMapper.insert(new FmsClosingVoucherDO().setClosingId(closing.getId()) + .setVoucherId(voucherId).setVoucherTime(voucherTime) + .setAmount(balance).setClosed(false).setAccountSetId(generateReqVO.getAccountSetId())); + return voucherId; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_VOUCHER_GENERATE_SUB_TYPE, + bizNo = "{{#generateReqVO.accountSetId}}", success = FMS_CLOSING_VOUCHER_BATCH_GENERATE_SUCCESS) + public List generateClosingVoucherList( + FmsClosingVoucherGenerateReqVO generateReqVO, Long userId) { + // 1.1 校验当前会计期间 + validateCurrentPeriod(generateReqVO.getAccountSetId(), generateReqVO.getMonth(), userId); + // 1.2 校验期间未结账 + if (closingPeriodService.isPeriodClosed(generateReqVO.getAccountSetId(), generateReqVO.getMonth())) { + throw exception(CLOSING_PERIOD_CLOSED); + } + // 1.3 校验结账方案 + List closings = new ArrayList<>(); + for (Long id : new LinkedHashSet<>(generateReqVO.getIds())) { + FmsClosingSchemeDO closing = closingSchemeService.validateClosingSchemeExists( + generateReqVO.getAccountSetId(), id); + if (ObjUtil.notEqual(closing.getType(), FmsClosingTypeEnum.PROFIT_LOSS.getType()) + && ObjUtil.notEqual(closing.getType(), FmsClosingTypeEnum.REGULAR.getType()) + && !isSpecialClosing(closing)) { + throw exception(CLOSING_SCHEME_NOT_EXISTS); + } + validatePeriodEndClosing(closing); + closings.add(closing); + } + + // 2. 统一删除本批方案的旧结转凭证,避免逐个重生成后删除旧凭证造成凭证号断号 + YearMonth month = LocalDateTimeUtils.parseYearMonth(generateReqVO.getMonth()); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + List existingVouchers = new ArrayList<>(); + for (FmsClosingSchemeDO closing : closings) { + existingVouchers.addAll(closingVoucherMapper.selectListByClosingIdAndPeriod( + closing.getId(), monthBeginTime, LocalDateTimeUtils.endOfMonth(monthBeginTime))); + } + deleteExistingClosingVouchers(generateReqVO.getAccountSetId(), existingVouchers, userId); + + // 3. 在同一事务中生成全部结转凭证 + List voucherIds = new ArrayList<>(); + for (FmsClosingSchemeDO closing : closings) { + boolean profitLoss = ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.PROFIT_LOSS.getType()); + Integer noBalanceErrorCode = profitLoss ? CLOSING_NO_PROFIT_LOSS_BALANCE.getCode() : CLOSING_SCHEME_NO_BALANCE.getCode(); + try { + if (profitLoss) { + FmsProfitLossGenerateReqVO profitLossReqVO = BeanUtils.toBean(generateReqVO, FmsProfitLossGenerateReqVO.class); + voucherIds.add(generateProfitLossVoucher(profitLossReqVO, userId)); + } else { + FmsClosingSchemeGenerateReqVO schemeReqVO = BeanUtils.toBean(generateReqVO, FmsClosingSchemeGenerateReqVO.class); + schemeReqVO.setId(closing.getId()); + voucherIds.add(generateClosingSchemeVoucher(schemeReqVO, userId)); + } + } catch (ServiceException ex) { + // 批量生成时,余额为零的方案无需生成凭证,继续处理其他方案 + if (ObjUtil.notEqual(ex.getCode(), noBalanceErrorCode)) { + throw ex; + } + } + } + return voucherIds; + } + + /** + * 删除方案当前期间的已生成结转凭证及其关联 + * + * @param accountSetId 账套编号 + * @param closingVouchers 结账凭证关联列表 + * @param userId 用户编号 + */ + private void deleteExistingClosingVouchers(Long accountSetId, List closingVouchers, Long userId) { + if (CollUtil.isEmpty(closingVouchers)) { + return; + } + List voucherIds = convertList(closingVouchers, FmsClosingVoucherDO::getVoucherId); + closingVoucherMapper.deleteByIds(convertList(closingVouchers, FmsClosingVoucherDO::getId)); + voucherService.deleteVoucherList(accountSetId, voucherIds, userId); + } + + /** + * 计算专用结转方案的结转金额 + * + * 转出未交增值税取应交增值税期末贷方净额;计提地税按应交增值税和消费税合计乘以比例; + * 计提所得税按利润总额减已计提所得税后乘以比例 + * + * @param closing 结账方案 + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param subjects 账套下的科目列表 + * @param balanceMap 科目编号到余额的 Map + * @param userId 用户编号 + * @return 结转金额 + */ + private BigDecimal calculateSpecialClosingBalance(FmsClosingSchemeDO closing, Long accountSetId, + YearMonth month, List subjects, + Map balanceMap, Long userId) { + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode, + Function.identity(), (first, second) -> first); + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.UNPAID_VAT.getType())) { + return getPositiveCreditEndingBalance(subjectMap.get(FmsClosingPresetSubjectEnum.VAT_PAYABLE.getCode()), balanceMap); + } + BigDecimal rate = getDirectionRatio( + closing.getSubjectRules(), FmsDebitCreditDirectionEnum.DEBIT.getType()); + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.LOCAL_TAX.getType())) { + BigDecimal taxBase = getPositiveCreditEndingBalance( + subjectMap.get(FmsClosingPresetSubjectEnum.VAT_PAYABLE.getCode()), balanceMap) + .add(getPositiveCreditEndingBalance( + subjectMap.get(FmsClosingPresetSubjectEnum.CONSUMPTION_TAX.getCode()), balanceMap)); + return taxBase.multiply(rate).divide(NumberUtils.ONE_HUNDRED, 2, RoundingMode.HALF_UP); + } + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.INCOME_TAX.getType())) { + FmsReportListReqVO listReqVO = new FmsReportListReqVO(); + listReqVO.setAccountSetId(accountSetId); + listReqVO.setStartMonth(month.toString()); + listReqVO.setEndMonth(month.toString()); + List reportItems = incomeStatementService.getIncomeStatement(listReqVO, userId); + Map reportItemMap = convertMap( + filterList(reportItems, item -> item.getRowNo() != null), + FmsReportItemRespVO::getRowNo, Function.identity(), (first, second) -> first); + BigDecimal totalProfit = reportItemMap.containsKey(30) + ? NumberUtils.zeroIfNull(reportItemMap.get(30).getYearAmount()) : BigDecimal.ZERO; + BigDecimal accruedIncomeTax = reportItemMap.containsKey(31) + ? NumberUtils.zeroIfNull(reportItemMap.get(31).getYearAmount()) : BigDecimal.ZERO; + return totalProfit.max(BigDecimal.ZERO).multiply(rate) + .divide(NumberUtils.ONE_HUNDRED, 2, RoundingMode.HALF_UP) + .subtract(accruedIncomeTax.max(BigDecimal.ZERO)).max(BigDecimal.ZERO); + } + return BigDecimal.ZERO; + } + + /** + * 获得科目期末贷方净额,借方大于贷方时按零处理 + * + * @param subject 科目 + * @param balanceMap 科目编号到余额的 Map + * @return 期末贷方净额 + */ + private BigDecimal getPositiveCreditEndingBalance(FmsSubjectDO subject, + Map balanceMap) { + // 预置科目不存在时直接抛错,避免按错误科目结转 + if (subject == null) { + throw exception(CLOSING_SPECIAL_SUBJECT_NOT_EXISTS); + } + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(subject.getId()); + return balance == null ? BigDecimal.ZERO : NumberUtils.zeroIfNull(balance.getEndingCreditAmount()) + .subtract(NumberUtils.zeroIfNull(balance.getEndingDebitAmount())).max(BigDecimal.ZERO); + } + + /** + * 校验结转损益的三个科目:调整科目必须为末级损益科目,结转科目必须为末级非损益科目 + * + * @param closing 结账方案 + * @param subjects 账套下的科目列表 + * @param parentIds 父级科目编号集合 + */ + private void validateProfitLossSubjects(FmsClosingSchemeDO closing, + List subjects, Set parentIds) { + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + FmsSubjectDO adjustmentSubject = subjectMap.get(closing.getPriorYearAdjustmentSubjectId()); + FmsSubjectDO adjustmentClosingSubject = subjectMap.get(closing.getAdjustmentClosingSubjectId()); + FmsSubjectDO otherClosingSubject = subjectMap.get(closing.getOtherClosingSubjectId()); + if (adjustmentSubject == null + || !ObjUtil.equal(adjustmentSubject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || parentIds.contains(adjustmentSubject.getId()) + || adjustmentClosingSubject == null + || parentIds.contains(adjustmentClosingSubject.getId()) + || ObjUtil.equal(adjustmentClosingSubject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || otherClosingSubject == null + || parentIds.contains(otherClosingSubject.getId()) + || ObjUtil.equal(otherClosingSubject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType())) { + throw exception(CLOSING_PROFIT_LOSS_SETTINGS_INVALID); + } + } + + /** + * 构造结转损益凭证分录 + * + * 逐个末级损益科目按余额方向生成结转分录,并汇总生成目标结转科目分录; + * gain 为 true 时只生成收益科目,false 时只生成损失科目,null 时合并生成 + * + * @param closing 结账方案 + * @param subjects 账套下的科目列表 + * @param parentIds 父级科目编号集合 + * @param balanceMap 科目编号到余额的 Map + * @param ledgerEntries 当前期间凭证分录列表 + * @param gain 是否收益科目,null 表示全部 + * @return 凭证分录列表 + */ + private List buildProfitLossEntries(FmsClosingSchemeDO closing, + List subjects, Set parentIds, + Map balanceMap, + List ledgerEntries, Boolean gain) { + List entries = new ArrayList<>(); + Map targetAmountMap = new LinkedHashMap<>(); + String digest = closing.getDigest(); + boolean reverseBalance = closing.getReverseBalance() == null || closing.getReverseBalance(); + // 1. 逐个末级损益科目生成结转分录,跳过无余额和不属于当前收益损失分类的科目 + for (FmsSubjectDO subject : subjects) { + if (ObjUtil.notEqual(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || parentIds.contains(subject.getId())) { + continue; + } + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(subject.getId()); + BigDecimal amount = balance == null ? BigDecimal.ZERO + : NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()) + .subtract(NumberUtils.zeroIfNull(balance.getPeriodCreditAmount())); + if (amount.signum() == 0 + || gain != null && gain != (amount.signum() < 0)) { + continue; + } + Long targetSubjectId = ObjUtil.equal(subject.getId(), + closing.getPriorYearAdjustmentSubjectId()) + ? closing.getAdjustmentClosingSubjectId() : closing.getOtherClosingSubjectId(); + Map, BigDecimal> auxiliaryAmountMap = + buildProfitLossAuxiliaryAmountMap(subject, amount, ledgerEntries); + auxiliaryAmountMap.forEach((auxiliaries, auxiliaryAmount) -> { + if (auxiliaryAmount.signum() == 0) { + return; + } + boolean normalCredit = ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.CREDIT.getType()); + boolean abnormalBalance = normalCredit && auxiliaryAmount.signum() > 0 + || !normalCredit && auxiliaryAmount.signum() < 0; + boolean sourceDebit = reverseBalance || abnormalBalance + ? auxiliaryAmount.signum() < 0 : normalCredit; + BigDecimal transferAmount = auxiliaryAmount.abs(); + FmsVoucherEntrySaveReqVO entry = buildEntry(digest, subject.getId(), + sourceDebit ? transferAmount : BigDecimal.ZERO, + sourceDebit ? BigDecimal.ZERO : transferAmount); + entry.setAuxiliaries(convertList(auxiliaries, auxiliary -> + new FmsVoucherEntrySaveReqVO.AuxiliaryItem() + .setTypeId(auxiliary.getTypeId()).setItemId(auxiliary.getItemId()))); + entries.add(entry); + targetAmountMap.merge(targetSubjectId, + sourceDebit ? transferAmount.negate() : transferAmount, BigDecimal::add); + }); + } + // 2. 按目标结转科目汇总生成分录 + targetAmountMap.forEach((subjectId, amount) -> { + if (amount.signum() != 0) { + entries.add(buildEntry(digest, subjectId, + amount.signum() > 0 ? amount : BigDecimal.ZERO, + amount.signum() < 0 ? amount.abs() : BigDecimal.ZERO)); + } + }); + return entries; + } + + /** + * 按辅助核算组合汇总损益科目发生额 + * + * 未启用辅助核算时直接返回科目汇总发生额;启用辅助核算时保留原分录的辅助核算组合,保证生成的结转分录满足科目辅助核算校验,并将每个辅助核算项目一并结平 + * + * @param subject 损益科目 + * @param amount 科目发生额 + * @param ledgerEntries 当前期间凭证分录列表 + * @return 辅助核算组合和发生额 Map + */ + private Map, BigDecimal> buildProfitLossAuxiliaryAmountMap( + FmsSubjectDO subject, BigDecimal amount, List ledgerEntries) { + if (CollUtil.isEmpty(subject.getAuxiliaryTypeIds())) { + return Collections.singletonMap(Collections.emptyList(), amount); + } + Map, BigDecimal> amountMap = new LinkedHashMap<>(); + for (FmsLedgerEntryVO entry : ledgerEntries) { + if (ObjUtil.notEqual(entry.getSubjectId(), subject.getId())) { + continue; + } + amountMap.merge(entry.getAuxiliaries(), NumberUtils.zeroIfNull(entry.getDebitAmount()) + .subtract(NumberUtils.zeroIfNull(entry.getCreditAmount())), BigDecimal::add); + } + return amountMap; + } + + /** + * 计算常规结账方案的来源科目结转金额 + * + * 按取数时间类型选择期初、期末、本期或本年累计口径,再按取数规则返回余额或发生额 + * + * @param closing 结账方案 + * @param balanceMap 科目编号到余额的 Map + * @return 结转金额 + */ + private BigDecimal calculateClosingSchemeBalance(FmsClosingSchemeDO closing, + Map balanceMap) { + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(closing.getSubjectId()); + if (balance == null) { + return BigDecimal.ZERO; + } + // 期初、年初或期末余额口径 + boolean openingBalance = ObjectUtils.equalsAny(closing.getTimeType(), + FmsClosingTimeTypeEnum.PERIOD_BEGIN.getType(), FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()); + BigDecimal debitBalance = openingBalance + ? NumberUtils.zeroIfNull(balance.getOpeningDebitAmount()) + : NumberUtils.zeroIfNull(balance.getEndingDebitAmount()); + BigDecimal creditBalance = openingBalance + ? NumberUtils.zeroIfNull(balance.getOpeningCreditAmount()) + : NumberUtils.zeroIfNull(balance.getEndingCreditAmount()); + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.BALANCE.getRule())) { + return debitBalance.subtract(creditBalance).abs(); + } + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + || ObjUtil.equal(closing.getFormulaRule(), + FmsFormulaRuleEnum.SUBJECT_DEBIT_BALANCE.getRule())) { + return debitBalance; + } + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.CREDIT_BALANCE.getRule()) + || ObjUtil.equal(closing.getFormulaRule(), + FmsFormulaRuleEnum.SUBJECT_CREDIT_BALANCE.getRule())) { + return creditBalance; + } + // 本期或本年累计发生额口径 + BigDecimal debitAmount = ObjUtil.equal(closing.getTimeType(), + FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()) + ? NumberUtils.zeroIfNull(balance.getYearDebitAmount()) + : NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()); + BigDecimal creditAmount = ObjUtil.equal(closing.getTimeType(), + FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()) + ? NumberUtils.zeroIfNull(balance.getYearCreditAmount()) + : NumberUtils.zeroIfNull(balance.getPeriodCreditAmount()); + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.DEBIT_AMOUNT.getRule())) { + return debitAmount; + } + if (ObjUtil.equal(closing.getFormulaRule(), FmsFormulaRuleEnum.CREDIT_AMOUNT.getRule())) { + return creditAmount; + } + return debitAmount.subtract(creditAmount).abs(); + } + + /** + * 按科目规则的金额比例构造常规结账凭证分录 + * + * @param closing 结账方案 + * @param balance 结转金额 + * @return 凭证分录列表 + */ + private List buildRegularClosingEntries( + FmsClosingSchemeDO closing, BigDecimal balance) { + // 逐条科目规则按比例分摊结转金额 + List entries = new ArrayList<>(); + for (FmsClosingSchemeDO.SubjectRule subjectRule : closing.getSubjectRules()) { + BigDecimal amount = balance.multiply(subjectRule.getAmountRatio()) + .divide(NumberUtils.ONE_HUNDRED, 2, RoundingMode.HALF_UP); + entries.add(buildEntry(subjectRule.getDigest(), subjectRule.getSubjectId(), + ObjUtil.equal(subjectRule.getDirection(), FmsDebitCreditDirectionEnum.DEBIT.getType()) + ? amount : BigDecimal.ZERO, + ObjUtil.equal(subjectRule.getDirection(), FmsDebitCreditDirectionEnum.CREDIT.getType()) + ? amount : BigDecimal.ZERO)); + } + return entries; + } + + /** + * 按科目规则的金额比例构造专用结转凭证分录 + * + * 借贷两侧分别按比例分摊,每侧最后一条分录倒挤剩余金额,保证合计等于结转金额 + * + * @param closing 结账方案 + * @param balance 结转金额 + * @return 凭证分录列表 + */ + private List buildSpecialClosingEntries( + FmsClosingSchemeDO closing, BigDecimal balance) { + BigDecimal debitRatio = getDirectionRatio( + closing.getSubjectRules(), FmsDebitCreditDirectionEnum.DEBIT.getType()); + BigDecimal creditRatio = getDirectionRatio( + closing.getSubjectRules(), FmsDebitCreditDirectionEnum.CREDIT.getType()); + int debitRemainingCount = (int) closing.getSubjectRules().stream() + .filter(item -> ObjUtil.equal(item.getDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType())).count(); + int creditRemainingCount = closing.getSubjectRules().size() - debitRemainingCount; + BigDecimal allocatedDebitAmount = BigDecimal.ZERO; + BigDecimal allocatedCreditAmount = BigDecimal.ZERO; + List entries = new ArrayList<>(); + for (FmsClosingSchemeDO.SubjectRule subjectRule : closing.getSubjectRules()) { + boolean debit = ObjUtil.equal(subjectRule.getDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()); + BigDecimal amount; + if (debit) { + debitRemainingCount--; + amount = debitRemainingCount == 0 ? balance.subtract(allocatedDebitAmount) + : balance.multiply(subjectRule.getAmountRatio()) + .divide(debitRatio, 2, RoundingMode.HALF_UP); + allocatedDebitAmount = allocatedDebitAmount.add(amount); + } else { + creditRemainingCount--; + amount = creditRemainingCount == 0 ? balance.subtract(allocatedCreditAmount) + : balance.multiply(subjectRule.getAmountRatio()) + .divide(creditRatio, 2, RoundingMode.HALF_UP); + allocatedCreditAmount = allocatedCreditAmount.add(amount); + } + entries.add(buildEntry(subjectRule.getDigest(), subjectRule.getSubjectId(), + debit ? amount : BigDecimal.ZERO, debit ? BigDecimal.ZERO : amount)); + } + return entries; + } + + /** + * 汇总科目规则中指定借贷方向的金额比例 + * + * @param subjectRules 科目规则 + * @param direction 借贷方向 + * @return 金额比例合计 + */ + private BigDecimal getDirectionRatio(List subjectRules, Integer direction) { + return sumBigDecimal(filterList(subjectRules, + item -> ObjUtil.equal(item.getDirection(), direction)), + FmsClosingSchemeDO.SubjectRule::getAmountRatio); + } + + /** + * 判断是否为专用结转方案(转出未交增值税、计提地税、计提所得税) + * + * @param closing 结账方案 + * @return 是否专用结转方案 + */ + private boolean isSpecialClosing(FmsClosingSchemeDO closing) { + return ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.UNPAID_VAT.getType()) + || ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.LOCAL_TAX.getType()) + || ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.INCOME_TAX.getType()); + } + + /** + * 获得单个会计期间的科目余额 Map + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + * @return 科目编号到余额的 Map + */ + private Map getSubjectBalanceMap( + Long accountSetId, YearMonth month, Long userId) { + return getSubjectBalanceMap(accountSetId, month, month, userId); + } + + /** + * 获得月份区间的科目余额 Map,树形余额扁平化后按科目编号存放 + * + * @param accountSetId 账套编号 + * @param startMonth 开始月份 + * @param endMonth 结束月份 + * @param userId 用户编号 + * @return 科目编号到余额的 Map + */ + private Map getSubjectBalanceMap( + Long accountSetId, YearMonth startMonth, YearMonth endMonth, Long userId) { + FmsLedgerListReqVO listReqVO = new FmsLedgerListReqVO().setAccountSetId(accountSetId) + .setStartMonth(startMonth.toString()).setEndMonth(endMonth.toString()); + Map result = new LinkedHashMap<>(); + flattenBalances(ledgerService.getSubjectBalanceList(listReqVO, userId), result); + return result; + } + + /** + * 递归扁平化树形科目余额到 Map + * + * @param balances 树形科目余额 + * @param result 科目编号到余额的 Map + */ + private void flattenBalances(List balances, + Map result) { + for (FmsLedgerSubjectBalanceRespVO balance : balances) { + result.put(balance.getSubjectId(), balance); + flattenBalances(balance.getChildren(), result); + } + } + + /** + * 构造凭证分录 + * + * @param digest 摘要 + * @param subjectId 科目编号 + * @param debitAmount 借方金额 + * @param creditAmount 贷方金额 + * @return 凭证分录 + */ + private FmsVoucherEntrySaveReqVO buildEntry(String digest, Long subjectId, BigDecimal debitAmount, BigDecimal creditAmount) { + return new FmsVoucherEntrySaveReqVO().setDigest(digest).setSubjectId(subjectId) + .setDebitAmount(debitAmount).setCreditAmount(creditAmount); + } + + /** + * 校验常规方案已启用期末结转 + * + * @param closing 结账方案 + */ + private void validatePeriodEndClosing(FmsClosingSchemeDO closing) { + if (ObjUtil.equal(closing.getType(), FmsClosingTypeEnum.REGULAR.getType()) + && !Boolean.TRUE.equals(closing.getPeriodEnd())) { + throw exception(CLOSING_SCHEME_NOT_PERIOD_END); + } + } + + /** + * 校验账套写权限、初始化状态,且指定期间为当前会计期间 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 用户编号 + * @return 已校验的账套 + */ + private FmsAccountSetDO validateCurrentPeriod(Long accountSetId, String month, Long userId) { + // 1. 校验账套写权限并锁定账套,避免同一账套并发生成结转凭证 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetWritePermission(accountSetId, userId); + accountSetService.lockAccountSet(accountSetId); + + // 2. 校验账套已完成初始化 + if (ObjUtil.notEqual(accountSet.getInitialized(), true) || accountSet.getStartTime() == null) { + throw exception(ACCOUNT_SET_NOT_INITIALIZED); + } + + // 3. 校验指定期间为账套当前会计期间 + String currentMonth = closingPeriodService.getCurrentMonth(accountSetId, accountSet.getStartTime()).toString(); + if (!ObjUtil.equal(currentMonth, month)) { + throw exception(CLOSING_NOT_CURRENT_PERIOD); + } + return accountSet; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetService.java new file mode 100644 index 0000000..4e7721b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetService.java @@ -0,0 +1,122 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * FMS 账套 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsAccountSetService { + + /** + * 创建账套 + * + * @param createReqVO 创建信息 + * @param userId 当前用户编号 + * @return 账套编号 + */ + Long createAccountSet(FmsAccountSetSaveReqVO createReqVO, Long userId); + + /** + * 更新账套 + * + * @param updateReqVO 更新信息 + * @param userId 当前用户编号 + */ + void updateAccountSet(FmsAccountSetSaveReqVO updateReqVO, Long userId); + + /** + * 更新账套会计制度 + * + * @param accountSetId 账套编号 + * @param standard 会计制度 + * @param userId 用户编号 + */ + void updateAccountSetStandard(Long accountSetId, Integer standard, Long userId); + + /** + * 初始化账套 + * + * @param initializeReqVO 初始化信息 + * @param userId 当前用户编号 + */ + void initializeAccountSet(FmsAccountSetInitializeReqVO initializeReqVO, Long userId); + + /** + * 获得账套 + * + * @param id 账套编号 + * @return 账套 + */ + FmsAccountSetDO getAccountSet(Long id); + + /** + * 校验账套存在并返回账套 + * + * @param id 账套编号 + * @return 账套 + */ + FmsAccountSetDO validateAccountSetExists(Long id); + + /** + * 校验账套读取权限 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 账套 + */ + FmsAccountSetDO validateAccountSetReadPermission(Long accountSetId, Long userId); + + /** + * 校验账套写入权限 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 账套 + */ + FmsAccountSetDO validateAccountSetWritePermission(Long accountSetId, Long userId); + + /** + * 校验账套主管权限 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 账套 + */ + FmsAccountSetDO validateAccountSetOwnerPermission(Long accountSetId, Long userId); + + /** + * 锁定账套,使同一账套的关键写操作串行执行 + * + * @param accountSetId 账套编号 + */ + void lockAccountSet(Long accountSetId); + + /** + * 获得指定账套列表 + * + * @param ids 账套编号数组 + * @return 账套列表 + */ + List getAccountSetList(Collection ids); + + /** + * 获得指定账套 Map + * + * @param ids 账套编号数组 + * @return 账套 Map + */ + default Map getAccountSetMap(Collection ids) { + return convertMap(getAccountSetList(ids), FmsAccountSetDO::getId); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetServiceImpl.java new file mode 100644 index 0000000..388c664 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetServiceImpl.java @@ -0,0 +1,230 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAccountSetMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountUserLevelEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsCurrencyPresetEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingTemplateService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.service.impl.DiffParseFunction; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_ALREADY_INITIALIZED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_ACCESS_DENIED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_CREATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_CREATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_INITIALIZE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_INITIALIZE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_UPDATE_SUCCESS; + +/** + * FMS 账套 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsAccountSetServiceImpl implements FmsAccountSetService { + + @Resource + private FmsAccountSetMapper accountSetMapper; + + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsAccountUserService accountUserService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsCurrencyService currencyService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsFinanceParameterService financeParameterService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private FmsVoucherWordService voucherWordService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsAuxiliaryTypeService auxiliaryTypeService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsSubjectService subjectService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsClosingSchemeService closingSchemeService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsFinanceIndicatorService financeIndicatorService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private FmsClosingTemplateService closingTemplateService; + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_ACCOUNT_SET_TYPE, subType = FMS_ACCOUNT_SET_CREATE_SUB_TYPE, + bizNo = "{{#accountSetId}}", success = FMS_ACCOUNT_SET_CREATE_SUCCESS) + public Long createAccountSet(FmsAccountSetSaveReqVO createReqVO, Long userId) { + // 1. 校验公司编码唯一 + validateAccountSetCompanyCodeUnique(null, createReqVO.getCompanyCode()); + + // 2. 创建未初始化账套 + FmsAccountSetDO accountSet = BeanUtils.toBean(createReqVO, FmsAccountSetDO.class) + .setId(null).setInitialized(false); + accountSetMapper.insert(accountSet); + + // 3. 将创建人加入账套 + accountUserService.createAccountOwner(accountSet.getId(), userId); + + // 4. 记录操作日志上下文 + LogRecordContext.putVariable("accountSetId", accountSet.getId()); + return accountSet.getId(); + } + + @Override + @LogRecord(type = FMS_ACCOUNT_SET_TYPE, subType = FMS_ACCOUNT_SET_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_ACCOUNT_SET_UPDATE_SUCCESS) + public void updateAccountSet(FmsAccountSetSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套主管权限 + FmsAccountSetDO accountSet = validateAccountSetOwnerPermission(updateReqVO.getId(), userId); + // 1.2 校验公司编码唯一 + validateAccountSetCompanyCodeUnique(updateReqVO.getId(), updateReqVO.getCompanyCode()); + + // 2. 更新账套基本信息 + accountSetMapper.updateById(BeanUtils.toBean(updateReqVO, FmsAccountSetDO.class)); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, + BeanUtils.toBean(accountSet, FmsAccountSetSaveReqVO.class)); + } + + @Override + public void updateAccountSetStandard(Long accountSetId, Integer standard, Long userId) { + // 1. 校验账套写权限 + validateAccountSetWritePermission(accountSetId, userId); + + // 2. 更新账套会计制度 + accountSetMapper.updateById(new FmsAccountSetDO().setId(accountSetId).setStandard(standard)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_ACCOUNT_SET_TYPE, subType = FMS_ACCOUNT_SET_INITIALIZE_SUB_TYPE, + bizNo = "{{#initializeReqVO.accountSetId}}", success = FMS_ACCOUNT_SET_INITIALIZE_SUCCESS) + public void initializeAccountSet(FmsAccountSetInitializeReqVO initializeReqVO, Long userId) { + // 1.1 校验账套成员写权限;未初始化账套由可维护成员完成初始化 + FmsAccountSetDO accountSet = validateAccountSetWritePermission(initializeReqVO.getAccountSetId(), userId); + // 1.2 校验账套初始化状态 + FmsCurrencyPresetEnum presetCurrency = FmsCurrencyPresetEnum.valueOfCode(initializeReqVO.getCurrencyCode()); + if (Boolean.TRUE.equals(accountSet.getInitialized())) { + throw exception(ACCOUNT_SET_ALREADY_INITIALIZED); + } + + // 2. 初始化账套基础数据 + FmsCurrencyDO currency = currencyService.initializeStandardCurrency(accountSet.getId(), presetCurrency); + financeParameterService.initializeFinanceParameter(accountSet.getId(), initializeReqVO); + voucherWordService.initializeDefaultVoucherWords(accountSet.getId()); + auxiliaryTypeService.initializeDefaultTypes(accountSet.getId()); + subjectService.initializeDefaultSubjects(accountSet.getId()); + closingTemplateService.initializeClosingTemplates(accountSet.getId(), userId); + closingSchemeService.initializeDefaultClosingSchemes(accountSet.getId(), userId); + financeIndicatorService.initializeDefaultFinanceIndicators(accountSet.getId(), userId); + + // 3. 回写账套初始化信息和本位币 + accountSetMapper.updateById(new FmsAccountSetDO().setId(accountSet.getId()) + .setInitialized(true).setStandard(initializeReqVO.getStandard()) + .setStartTime(LocalDateTimeUtils.beginOfMonth(initializeReqVO.getStartTime())) + .setCurrencyId(currency.getId())); + + // 4. 记录操作日志上下文 + LogRecordContext.putVariable("accountSet", accountSet); + } + + @Override + public FmsAccountSetDO getAccountSet(Long id) { + return accountSetMapper.selectById(id); + } + + @Override + public FmsAccountSetDO validateAccountSetExists(Long id) { + FmsAccountSetDO accountSet = getAccountSet(id); + if (accountSet == null) { + throw exception(ACCOUNT_SET_NOT_EXISTS); + } + return accountSet; + } + + @Override + public FmsAccountSetDO validateAccountSetReadPermission(Long accountSetId, Long userId) { + FmsAccountSetDO accountSet = validateAccountSetExists(accountSetId); + FmsAccountUserDO accountUser = accountUserService.getAccountUser(accountSetId, userId); + if (accountUser == null || !FmsAccountUserLevelEnum.isReadable(accountUser.getLevel())) { + throw exception(ACCOUNT_SET_ACCESS_DENIED); + } + return accountSet; + } + + @Override + public FmsAccountSetDO validateAccountSetWritePermission(Long accountSetId, Long userId) { + FmsAccountSetDO accountSet = validateAccountSetExists(accountSetId); + FmsAccountUserDO accountUser = accountUserService.getAccountUser(accountSetId, userId); + if (accountUser == null || !FmsAccountUserLevelEnum.isWritable(accountUser.getLevel())) { + throw exception(ACCOUNT_SET_ACCESS_DENIED); + } + return accountSet; + } + + @Override + public void lockAccountSet(Long accountSetId) { + FmsAccountSetDO accountSet = accountSetMapper.selectByIdForUpdate(accountSetId); + if (accountSet == null) { + throw exception(ACCOUNT_SET_NOT_EXISTS); + } + } + + @Override + public FmsAccountSetDO validateAccountSetOwnerPermission(Long accountSetId, Long userId) { + FmsAccountSetDO accountSet = validateAccountSetExists(accountSetId); + FmsAccountUserDO accountUser = accountUserService.getAccountUser(accountSetId, userId); + if (accountUser == null || !FmsAccountUserLevelEnum.isOwner(accountUser.getLevel())) { + throw exception(ACCOUNT_SET_ACCESS_DENIED); + } + return accountSet; + } + + @Override + public List getAccountSetList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return accountSetMapper.selectByIds(ids); + } + + private void validateAccountSetCompanyCodeUnique(Long id, String companyCode) { + FmsAccountSetDO accountSet = accountSetMapper.selectByCompanyCode(companyCode); + if (accountSet != null && ObjUtil.notEqual(id, accountSet.getId())) { + throw exception(ACCOUNT_SET_CODE_DUPLICATE); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserService.java new file mode 100644 index 0000000..aee1dca --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserService.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser.FmsAccountUserUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; + +import java.util.List; + +/** + * FMS 账套用户 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsAccountUserService { + + /** + * 创建账套主管关系 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + */ + void createAccountOwner(Long accountSetId, Long userId); + + /** + * 获得当前用户的账套关系列表 + * + * @param userId 用户编号 + * @return 账套关系列表 + */ + List getAccountUserList(Long userId); + + /** + * 获得账套用户关系 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 账套用户关系 + */ + FmsAccountUserDO getAccountUser(Long accountSetId, Long userId); + + /** + * 设置当前用户的默认账套 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + */ + void updateAccountSetDefaultStatus(Long accountSetId, Long userId); + + /** + * 获得账套成员列表 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 账套成员列表 + */ + List getAccountUserList(Long accountSetId, Long userId); + + /** + * 更新账套成员列表 + * + * @param updateReqVO 更新信息 + * @param userId 当前用户编号 + */ + void updateAccountUserList(FmsAccountUserUpdateReqVO updateReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserServiceImpl.java new file mode 100644 index 0000000..ff242fc --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserServiceImpl.java @@ -0,0 +1,140 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser.FmsAccountUserUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAccountUserMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountUserLevelEnum; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_MEMBER_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_MEMBER_UPDATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_ACCOUNT_SET_TYPE; + +/** + * FMS 账套用户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsAccountUserServiceImpl implements FmsAccountUserService { + + @Resource + private FmsAccountUserMapper accountUserMapper; + + @Resource + @Lazy // 延迟加载,避免与账套 Service 循环依赖 + private FmsAccountSetService accountSetService; + + @Resource + private AdminUserApi adminUserApi; + + @Override + public void createAccountOwner(Long accountSetId, Long userId) { + boolean defaultStatus = accountUserMapper.selectCountByUserId(userId) == 0; + FmsAccountUserDO accountUser = new FmsAccountUserDO().setAccountSetId(accountSetId).setUserId(userId) + .setDefaultStatus(defaultStatus).setFounder(true) + .setLevel(FmsAccountUserLevelEnum.OWNER.getLevel()); + accountUserMapper.insert(accountUser); + } + + @Override + public List getAccountUserList(Long userId) { + return accountUserMapper.selectListByUserId(userId); + } + + @Override + public FmsAccountUserDO getAccountUser(Long accountSetId, Long userId) { + return accountUserMapper.selectByAccountSetIdAndUserId(accountSetId, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAccountSetDefaultStatus(Long accountSetId, Long userId) { + // 1. 校验用户属于目标账套 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 清除用户原默认账套,再设置目标账套 + accountUserMapper.updateDefaultStatusByUserId(userId); + accountUserMapper.updateDefaultStatusByAccountSetIdAndUserId(accountSetId, userId); + } + + @Override + public List getAccountUserList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询账套成员 + return accountUserMapper.selectListByAccountSetId(accountSetId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_ACCOUNT_SET_TYPE, subType = FMS_ACCOUNT_SET_MEMBER_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.accountSetId}}", success = FMS_ACCOUNT_SET_MEMBER_UPDATE_SUCCESS) + public void updateAccountUserList(FmsAccountUserUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套主管权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetOwnerPermission( + updateReqVO.getAccountSetId(), userId); + // 1.2 查询现有成员并校验授权成员 + List accountUsers = accountUserMapper.selectListByAccountSetId(accountSet.getId()); + Set founderUserIds = convertSet(accountUsers, FmsAccountUserDO::getUserId, + accountUser -> Boolean.TRUE.equals(accountUser.getFounder())); + Map memberLevelMap = convertMap(updateReqVO.getMembers(), + FmsAccountUserUpdateReqVO.Member::getUserId, FmsAccountUserUpdateReqVO.Member::getLevel); + Set memberUserIds = new HashSet<>(memberLevelMap.keySet()); + memberUserIds.removeAll(founderUserIds); + adminUserApi.validateUserList(memberUserIds); + + // 2.1 移出取消授权的成员 + Set currentMemberUserIds = convertSet(accountUsers, FmsAccountUserDO::getUserId, + accountUser -> Boolean.FALSE.equals(accountUser.getFounder())); + Set deleteUserIds = new HashSet<>(currentMemberUserIds); + deleteUserIds.removeAll(memberUserIds); + if (CollUtil.isNotEmpty(deleteUserIds)) { + accountUserMapper.deleteByAccountSetIdAndUserIds(accountSet.getId(), deleteUserIds); + } + + // 2.2 更新已有成员的权限级别 + List updateAccountUsers = convertList(accountUsers, + accountUser -> new FmsAccountUserDO().setId(accountUser.getId()) + .setLevel(memberLevelMap.get(accountUser.getUserId())), + accountUser -> Boolean.FALSE.equals(accountUser.getFounder()) + && memberLevelMap.containsKey(accountUser.getUserId()) + && ObjUtil.notEqual(accountUser.getLevel(), memberLevelMap.get(accountUser.getUserId()))); + if (CollUtil.isNotEmpty(updateAccountUsers)) { + accountUserMapper.updateBatch(updateAccountUsers); + } + + // 2.3 加入新增授权的成员 + Set createUserIds = new HashSet<>(memberUserIds); + createUserIds.removeAll(currentMemberUserIds); + if (CollUtil.isNotEmpty(createUserIds)) { + accountUserMapper.insertBatch(convertList(createUserIds, + memberUserId -> new FmsAccountUserDO().setAccountSetId(accountSet.getId()) + .setUserId(memberUserId).setDefaultStatus(false).setFounder(false) + .setLevel(memberLevelMap.get(memberUserId)))); + } + + // 4. 记录操作日志上下文 + LogRecordContext.putVariable("accountSet", accountSet); + } +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationService.java new file mode 100644 index 0000000..041e0f8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationService.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 辅助核算组合 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsAuxiliaryCombinationService { + + /** + * 为凭证分录获得或创建辅助核算组合 + * + * @param accountSetId 账套编号 + * @param entries 凭证分录数组 + */ + void setAuxiliaryCombinationIds(Long accountSetId, List entries); + + /** + * 保存辅助核算组合 + * + * @param accountSetId 账套编号 + * @param subjectId 科目编号 + * @param items 辅助核算项目数组 + * @return 辅助核算组合 + */ + FmsAuxiliaryCombinationDO saveAuxiliaryCombination(Long accountSetId, Long subjectId, + List items); + + /** + * 获得指定科目的辅助核算组合数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号数组 + * @return 辅助核算组合数量 + */ + Long getAuxiliaryCombinationCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 迁移科目的辅助核算组合 + * + * @param accountSetId 账套编号 + * @param subjectId 科目编号 + * @param targetSubjectId 目标科目编号 + */ + void updateAuxiliaryCombinationSubject(Long accountSetId, Long subjectId, Long targetSubjectId); + + /** + * 删除包含指定辅助核算项目的组合 + * + * @param accountSetId 账套编号 + * @param auxiliaryItemIds 辅助核算项目编号数组 + */ + void deleteAuxiliaryCombinationByAuxiliaryItemIds(Long accountSetId, Collection auxiliaryItemIds); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationServiceImpl.java new file mode 100644 index 0000000..2b54cf9 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationServiceImpl.java @@ -0,0 +1,141 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAuxiliaryCombinationMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * FMS 辅助核算组合 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsAuxiliaryCombinationServiceImpl implements FmsAuxiliaryCombinationService { + + @Resource + private FmsAuxiliaryCombinationMapper auxiliaryCombinationMapper; + + @Override + public void setAuxiliaryCombinationIds(Long accountSetId, List entries) { + // 1. 一次加载账套已有的辅助核算组合 + List combinations = auxiliaryCombinationMapper.selectListByAccountSetId(accountSetId); + + // 2. 逐条复用或创建组合并回填分录 + for (FmsVoucherEntryDO entry : entries) { + // 2.1 无辅助核算项目时,清空分录的组合编号 + if (CollUtil.isEmpty(entry.getAuxiliaries())) { + entry.setAssistCombinationId(null); + continue; + } + // 2.2 转换辅助核算组合明细 + List items = convertList(entry.getAuxiliaries(), + auxiliary -> FmsAuxiliaryCombinationDO.AuxiliaryItem.builder() + .type(auxiliary.getType()).typeId(auxiliary.getTypeId()) + .itemId(auxiliary.getItemId()).name(auxiliary.getName()).build()); + // 2.3 复用或创建辅助核算组合 + FmsAuxiliaryCombinationDO combination = CollUtil.findOne(combinations, item -> + ObjUtil.equal(item.getSubjectId(), entry.getSubjectId()) && isSameCombination(item.getItems(), items)); + if (combination == null) { + combination = new FmsAuxiliaryCombinationDO().setAccountSetId(accountSetId) + .setSubjectId(entry.getSubjectId()).setItems(items); + auxiliaryCombinationMapper.insert(combination); + combinations.add(combination); + } + // 2.4 回填分录的辅助核算组合编号 + entry.setAssistCombinationId(combination.getId()); + } + } + + @Override + public FmsAuxiliaryCombinationDO saveAuxiliaryCombination(Long accountSetId, Long subjectId, + List items) { + // 1. 查询已有组合 + FmsAuxiliaryCombinationDO combination = CollUtil.findOne( + auxiliaryCombinationMapper.selectListByAccountSetId(accountSetId), + item -> ObjUtil.equal(item.getSubjectId(), subjectId) + && isSameCombination(item.getItems(), items)); + if (combination != null) { + return combination; + } + + // 2. 创建组合 + combination = new FmsAuxiliaryCombinationDO().setAccountSetId(accountSetId) + .setSubjectId(subjectId).setItems(items); + auxiliaryCombinationMapper.insert(combination); + return combination; + } + + @Override + public Long getAuxiliaryCombinationCountBySubjectIds(Long accountSetId, Collection subjectIds) { + return auxiliaryCombinationMapper.selectCountByAccountSetIdAndSubjectIds(accountSetId, subjectIds); + } + + @Override + public void updateAuxiliaryCombinationSubject(Long accountSetId, Long subjectId, Long targetSubjectId) { + auxiliaryCombinationMapper.updateSubject(accountSetId, subjectId, + new FmsAuxiliaryCombinationDO().setSubjectId(targetSubjectId)); + } + + @Override + public void deleteAuxiliaryCombinationByAuxiliaryItemIds( + Long accountSetId, Collection auxiliaryItemIds) { + // 1. 校验辅助核算项目编号数组非空 + if (CollUtil.isEmpty(auxiliaryItemIds)) { + return; + } + + // 2. 查询包含指定辅助核算项目的组合 + Set auxiliaryItemIdSet = new HashSet<>(auxiliaryItemIds); + List combinations = auxiliaryCombinationMapper + .selectListByAccountSetId(accountSetId); + List combinationIds = convertList(combinations, FmsAuxiliaryCombinationDO::getId, + combination -> CollUtil.isNotEmpty(combination.getItems()) + && anyMatch(combination.getItems(), + item -> auxiliaryItemIdSet.contains(item.getItemId()))); + + // 3. 删除辅助核算组合 + if (CollUtil.isNotEmpty(combinationIds)) { + auxiliaryCombinationMapper.deleteByIds(combinationIds); + } + } + + /** + * 判断两个辅助核算组合是否相同 + * + * @param left 左侧辅助核算项目数组 + * @param right 右侧辅助核算项目数组 + * @return 是否相同 + */ + private boolean isSameCombination(List left, + List right) { + // 项目数量不同,不是同一组合 + if (CollUtil.size(left) != CollUtil.size(right)) { + return false; + } + // 按辅助核算类别和项目的固定顺序逐项比较 + for (int index = 0; index < CollUtil.size(left); index++) { + FmsAuxiliaryCombinationDO.AuxiliaryItem leftItem = left.get(index); + FmsAuxiliaryCombinationDO.AuxiliaryItem rightItem = right.get(index); + if (ObjUtil.notEqual(leftItem.getTypeId(), rightItem.getTypeId()) + || ObjUtil.notEqual(leftItem.getItemId(), rightItem.getItemId())) { + return false; + } + } + return true; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemService.java new file mode 100644 index 0000000..150dfd3 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemService.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; + +import java.util.List; +import java.util.Collection; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * FMS 辅助核算项目 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsAuxiliaryItemService { + + /** + * 创建辅助核算项目 + * + * @param createReqVO 创建信息 + * @param userId 当前用户编号 + * @return 编号 + */ + Long createAuxiliaryItem(FmsAuxiliaryItemSaveReqVO createReqVO, Long userId); + + /** + * 更新辅助核算项目 + * + * @param updateReqVO 更新信息 + * @param userId 当前用户编号 + */ + void updateAuxiliaryItem(FmsAuxiliaryItemSaveReqVO updateReqVO, Long userId); + + /** + * 删除辅助核算项目 + * + * @param accountSetId 账套编号 + * @param ids 编号数组 + * @param userId 当前用户编号 + */ + void deleteAuxiliaryItemList(Long accountSetId, List ids, Long userId); + + /** + * 更新辅助核算项目状态 + * + * @param accountSetId 账套编号 + * @param id 编号 + * @param status 状态 + * @param userId 当前用户编号 + */ + void updateAuxiliaryItemStatus(Long accountSetId, Long id, Integer status, Long userId); + + /** + * 导入辅助核算项目 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @param importItems 导入项目数组 + * @param userId 当前用户编号 + * @return 导入结果 + */ + FmsAuxiliaryItemImportRespVO importAuxiliaryItemList(Long accountSetId, Long auxiliaryTypeId, + List importItems, Long userId); + + /** + * 获得辅助核算项目分页 + * + * @param pageReqVO 分页条件 + * @param userId 当前用户编号 + * @return 辅助核算项目分页 + */ + PageResult getAuxiliaryItemPage( + FmsAuxiliaryItemPageReqVO pageReqVO, Long userId); + + /** + * 获得辅助核算项目 + * + * @param accountSetId 账套编号 + * @param id 辅助核算项目编号 + * @param userId 当前用户编号 + * @return 辅助核算项目 + */ + FmsAuxiliaryItemDO getAuxiliaryItem(Long accountSetId, Long id, Long userId); + + /** + * 获得辅助核算项目列表 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @param userId 当前用户编号 + * @return 辅助核算项目列表 + */ + List getAuxiliaryItemList( + Long accountSetId, Long auxiliaryTypeId, Long userId); + + /** + * 获得指定状态的辅助核算项目列表 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @param status 状态 + * @param userId 当前用户编号 + * @return 辅助核算项目列表 + */ + List getAuxiliaryItemListByAccountSetIdAndAuxiliaryTypeIdAndStatus( + Long accountSetId, Long auxiliaryTypeId, Integer status, Long userId); + + /** + * 获得账套的全部辅助核算项目 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 辅助核算项目列表 + */ + List getAuxiliaryItemListByAccountSetId(Long accountSetId, Long userId); + + /** + * 获得账套的辅助核算项目 Map + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 辅助核算项目 Map,键为“类别编号|项目编码” + */ + default Map getAuxiliaryItemMapByTypeIdAndCode( + Long accountSetId, Long userId) { + return convertMap(getAuxiliaryItemListByAccountSetId(accountSetId, userId), + item -> item.getAuxiliaryTypeId() + "|" + item.getCode()); + } + + /** + * 获得指定辅助核算类别的项目数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @return 辅助核算项目数量 + */ + Long getAuxiliaryItemCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId); + + /** + * 校验并获得辅助核算项目列表 + * + * @param accountSetId 账套编号 + * @param ids 辅助核算项目编号数组 + * @return 辅助核算项目列表 + */ + List validateAuxiliaryItemList(Long accountSetId, Collection ids); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemServiceImpl.java new file mode 100644 index 0000000..61419f7 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemServiceImpl.java @@ -0,0 +1,329 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAuxiliaryItemMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolationException; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; + +/** + * FMS 辅助核算项目 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsAuxiliaryItemServiceImpl implements FmsAuxiliaryItemService { + + @Resource + private FmsAuxiliaryItemMapper auxiliaryItemMapper; + + @Resource + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private FmsVoucherService voucherService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private FmsVoucherTemplateService voucherTemplateService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private FmsInitialBalanceService initialBalanceService; + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_AUXILIARY_ITEM_TYPE, subType = FMS_AUXILIARY_ITEM_CREATE_SUB_TYPE, + bizNo = "{{#auxiliaryItemId}}", success = FMS_AUXILIARY_ITEM_CREATE_SUCCESS) + public Long createAuxiliaryItem(FmsAuxiliaryItemSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验辅助核算类别和编码唯一 + validateAuxiliaryType(createReqVO.getAccountSetId(), createReqVO.getAuxiliaryTypeId()); + validateAuxiliaryItemCodeUnique(null, createReqVO.getAccountSetId(), + createReqVO.getAuxiliaryTypeId(), createReqVO.getCode()); + + // 2. 创建辅助核算项目 + FmsAuxiliaryItemDO auxiliaryItem = BeanUtils.toBean(createReqVO, FmsAuxiliaryItemDO.class) + .setId(null).setStatus(CommonStatusEnum.ENABLE.getStatus()); + auxiliaryItemMapper.insert(auxiliaryItem); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("auxiliaryItemId", auxiliaryItem.getId()); + return auxiliaryItem.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_AUXILIARY_ITEM_TYPE, subType = FMS_AUXILIARY_ITEM_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_AUXILIARY_ITEM_UPDATE_SUCCESS) + public void updateAuxiliaryItem(FmsAuxiliaryItemSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验辅助核算项目和所属类别 + FmsAuxiliaryItemDO auxiliaryItem = validateAuxiliaryItemExists( + updateReqVO.getAccountSetId(), updateReqVO.getId()); + if (ObjUtil.notEqual(auxiliaryItem.getAuxiliaryTypeId(), updateReqVO.getAuxiliaryTypeId())) { + throw exception(AUXILIARY_TYPE_NOT_EXISTS); + } + validateAuxiliaryType(updateReqVO.getAccountSetId(), updateReqVO.getAuxiliaryTypeId()); + // 1.3 校验编码唯一 + validateAuxiliaryItemCodeUnique(auxiliaryItem.getId(), updateReqVO.getAccountSetId(), + updateReqVO.getAuxiliaryTypeId(), updateReqVO.getCode()); + + // 2. 更新辅助核算项目 + auxiliaryItemMapper.updateById(BeanUtils.toBean(updateReqVO, FmsAuxiliaryItemDO.class) + .setStatus(auxiliaryItem.getStatus())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_AUXILIARY_ITEM_TYPE, subType = FMS_AUXILIARY_ITEM_DELETE_SUB_TYPE, + bizNo = "{{#accountSetId}}", success = FMS_AUXILIARY_ITEM_DELETE_SUCCESS) + public void deleteAuxiliaryItemList(Long accountSetId, List ids, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验辅助核算项目 + List auxiliaryItems = auxiliaryItemMapper + .selectListByIdsAndAccountSetId(ids, accountSetId); + if (auxiliaryItems.size() != new LinkedHashSet<>(ids).size()) { + throw exception(AUXILIARY_ITEM_NOT_EXISTS); + } + Set idSet = new LinkedHashSet<>(ids); + // 1.3 校验辅助核算项目未被业务引用 + // 1.3.1 校验辅助核算项目未被凭证使用 + Long voucherEntryCount = voucherService.getVoucherEntryCountByAuxiliaryItemIds(accountSetId, idSet); + if (voucherEntryCount > 0) { + throw exception(AUXILIARY_ITEM_VOUCHER_IN_USE, voucherEntryCount); + } + // 1.3.2 校验辅助核算项目未被凭证模板使用 + Long voucherTemplateCount = voucherTemplateService + .getVoucherTemplateCountByAuxiliaryItemIds(accountSetId, idSet); + if (voucherTemplateCount > 0) { + throw exception(AUXILIARY_ITEM_VOUCHER_TEMPLATE_IN_USE, voucherTemplateCount); + } + // 1.3.3 校验辅助核算项目未被初始余额使用 + Long initialBalanceCount = initialBalanceService + .getInitialBalanceCountByAuxiliaryItemIds(accountSetId, idSet); + if (initialBalanceCount > 0) { + throw exception(AUXILIARY_ITEM_INITIAL_BALANCE_IN_USE, initialBalanceCount); + } + // 2. 删除派生的辅助核算组合和辅助核算项目 + auxiliaryCombinationService.deleteAuxiliaryCombinationByAuxiliaryItemIds(accountSetId, idSet); + auxiliaryItemMapper.deleteByIds(ids); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("auxiliaryItem", CollUtil.getFirst(auxiliaryItems)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_AUXILIARY_ITEM_TYPE, subType = FMS_AUXILIARY_ITEM_STATUS_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_AUXILIARY_ITEM_STATUS_SUCCESS) + public void updateAuxiliaryItemStatus(Long accountSetId, Long id, Integer status, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验辅助核算项目 + FmsAuxiliaryItemDO auxiliaryItem = validateAuxiliaryItemExists(accountSetId, id); + + // 2. 更新状态 + auxiliaryItemMapper.updateById(new FmsAuxiliaryItemDO() + .setId(auxiliaryItem.getId()).setStatus(status)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FmsAuxiliaryItemImportRespVO importAuxiliaryItemList(Long accountSetId, Long auxiliaryTypeId, + List importItems, Long userId) { + // 1.1 校验导入数据非空 + if (CollUtil.isEmpty(importItems)) { + throw exception(AUXILIARY_ITEM_IMPORT_LIST_IS_EMPTY); + } + // 1.2 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.3 校验辅助核算类别 + FmsAuxiliaryTypeDO auxiliaryType = CollUtil.getFirst(auxiliaryTypeService + .validateAuxiliaryTypeList(accountSetId, Collections.singletonList(auxiliaryTypeId))); + // 1.4 加载已有辅助核算项目 + Map itemMap = convertMap( + auxiliaryItemMapper.selectListByAccountSetIdAndAuxiliaryTypeId(accountSetId, auxiliaryTypeId), + FmsAuxiliaryItemDO::getCode); + + // 2. 逐行导入辅助核算项目 + Set importCodes = new HashSet<>(); + FmsAuxiliaryItemImportRespVO respVO = new FmsAuxiliaryItemImportRespVO() + .setTotalCount(importItems.size()).setSuccessItemCodes(new ArrayList<>()) + .setFailureReasons(new LinkedHashMap<>()); + for (int index = 0; index < importItems.size(); index++) { + FmsAuxiliaryItemImportExcelVO importItem = importItems.get(index); + String label = buildImportLabel(index + 2, importItem.getCode(), importItem.getName()); + try { + // 2.1 校验导入字段和编码唯一 + ValidationUtils.validate(importItem); + if (!importCodes.add(importItem.getCode())) { + throw new IllegalArgumentException("导入文件内编码重复"); + } + if (itemMap.containsKey(importItem.getCode())) { + throw new IllegalArgumentException("编码已存在"); + } + + // 2.2 构造并创建辅助核算项目 + FmsAuxiliaryItemDO item = new FmsAuxiliaryItemDO().setAccountSetId(accountSetId) + .setAuxiliaryTypeId(auxiliaryTypeId).setCode(importItem.getCode()) + .setName(importItem.getName()).setRemark(importItem.getRemark()) + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + if (FmsAuxiliaryTypeEnum.INVENTORY.getType().equals(auxiliaryType.getType())) { + item.setSpecification(importItem.getSpecification()).setUnit(importItem.getUnit()); + } + auxiliaryItemMapper.insert(item); + itemMap.put(importItem.getCode(), item); + + // 2.3 记录导入结果 + respVO.getSuccessItemCodes().add(importItem.getCode()); + } catch (ConstraintViolationException exception) { + respVO.getFailureReasons().put(label, exception.getMessage()); + } catch (RuntimeException exception) { + respVO.getFailureReasons().put( + label, StrUtil.blankToDefault(exception.getMessage(), "导入失败")); + } + } + return respVO; + } + + @Override + public PageResult getAuxiliaryItemPage(FmsAuxiliaryItemPageReqVO pageReqVO, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(pageReqVO.getAccountSetId(), userId); + // 1.2 校验辅助核算类别 + validateAuxiliaryType(pageReqVO.getAccountSetId(), pageReqVO.getAuxiliaryTypeId()); + + // 2. 查询辅助核算项目分页 + return auxiliaryItemMapper.selectPage(pageReqVO); + } + + @Override + public FmsAuxiliaryItemDO getAuxiliaryItem(Long accountSetId, Long id, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询并校验辅助核算项目 + return validateAuxiliaryItemExists(accountSetId, id); + } + + @Override + public List getAuxiliaryItemList(Long accountSetId, Long auxiliaryTypeId, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 校验辅助核算类别 + validateAuxiliaryType(accountSetId, auxiliaryTypeId); + + // 2. 查询辅助核算项目 + return auxiliaryItemMapper.selectListByAccountSetIdAndAuxiliaryTypeId( + accountSetId, auxiliaryTypeId); + } + + @Override + public List getAuxiliaryItemListByAccountSetIdAndAuxiliaryTypeIdAndStatus( + Long accountSetId, Long auxiliaryTypeId, Integer status, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 校验辅助核算类别 + validateAuxiliaryType(accountSetId, auxiliaryTypeId); + + // 2. 查询指定状态的辅助核算项目 + return auxiliaryItemMapper.selectListByAccountSetIdAndAuxiliaryTypeIdAndStatus( + accountSetId, auxiliaryTypeId, status); + } + + @Override + public List getAuxiliaryItemListByAccountSetId(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询账套的全部辅助核算项目 + return auxiliaryItemMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public Long getAuxiliaryItemCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId) { + return auxiliaryItemMapper.selectCountByAuxiliaryTypeId(accountSetId, auxiliaryTypeId); + } + + @Override + public List validateAuxiliaryItemList(Long accountSetId, Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List items = auxiliaryItemMapper.selectListByIdsAndAccountSetId(ids, accountSetId); + if (items.size() != new LinkedHashSet<>(ids).size()) { + throw exception(AUXILIARY_ITEM_NOT_EXISTS); + } + return items; + } + + private FmsAuxiliaryItemDO validateAuxiliaryItemExists(Long accountSetId, Long id) { + FmsAuxiliaryItemDO auxiliaryItem = auxiliaryItemMapper.selectById(id); + if (auxiliaryItem == null || ObjUtil.notEqual(auxiliaryItem.getAccountSetId(), accountSetId)) { + throw exception(AUXILIARY_ITEM_NOT_EXISTS); + } + return auxiliaryItem; + } + + private void validateAuxiliaryType(Long accountSetId, Long auxiliaryTypeId) { + auxiliaryTypeService.validateAuxiliaryTypeList(accountSetId, Collections.singletonList(auxiliaryTypeId)); + } + + private void validateAuxiliaryItemCodeUnique(Long id, Long accountSetId, Long auxiliaryTypeId, String code) { + FmsAuxiliaryItemDO auxiliaryItem = auxiliaryItemMapper + .selectByTypeIdAndCode(accountSetId, auxiliaryTypeId, code); + if (auxiliaryItem != null && ObjUtil.notEqual(auxiliaryItem.getId(), id)) { + throw exception(AUXILIARY_ITEM_CODE_DUPLICATE); + } + } + + private String buildImportLabel(Integer rowNumber, String code, String name) { + return "第 " + rowNumber + " 行(" + StrUtil.blankToDefault(code, "未填写编码") + + (StrUtil.isBlank(name) ? "" : " " + name) + ")"; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeService.java new file mode 100644 index 0000000..d762bac --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeService.java @@ -0,0 +1,136 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype.FmsAuxiliaryTypeSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * FMS 辅助核算类别 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsAuxiliaryTypeService { + + /** + * 初始化系统预置辅助核算类别 + * + * @param accountSetId 账套编号 + */ + void initializeDefaultTypes(Long accountSetId); + + /** + * 创建辅助核算类别 + * + * @param createReqVO 创建信息 + * @param userId 当前用户编号 + * @return 编号 + */ + Long createAuxiliaryType(FmsAuxiliaryTypeSaveReqVO createReqVO, Long userId); + + /** + * 更新辅助核算类别 + * + * @param updateReqVO 更新信息 + * @param userId 当前用户编号 + */ + void updateAuxiliaryType(FmsAuxiliaryTypeSaveReqVO updateReqVO, Long userId); + + /** + * 删除辅助核算类别 + * + * @param accountSetId 账套编号 + * @param id 编号 + * @param userId 当前用户编号 + */ + void deleteAuxiliaryType(Long accountSetId, Long id, Long userId); + + /** + * 获得辅助核算类别 + * + * @param accountSetId 账套编号 + * @param id 辅助核算类别编号 + * @param userId 当前用户编号 + * @return 辅助核算类别 + */ + FmsAuxiliaryTypeDO getAuxiliaryType(Long accountSetId, Long id, Long userId); + + /** + * 获得辅助核算类别列表 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 辅助核算类别列表 + */ + List getAuxiliaryTypeList(Long accountSetId, Long userId); + + /** + * 获得账套的辅助核算类别列表 + * + * @param accountSetId 账套编号 + * @return 辅助核算类别列表 + */ + List getAuxiliaryTypeList(Long accountSetId); + + /** + * 获得辅助核算类别列表 + * + * @param ids 辅助核算类别编号数组 + * @return 辅助核算类别列表 + */ + List getAuxiliaryTypeList(Collection ids); + + /** + * 获得辅助核算类别 Map + * + * @param ids 辅助核算类别编号数组 + * @return 辅助核算类别 Map + */ + default Map getAuxiliaryTypeMap(Collection ids) { + return convertMap(getAuxiliaryTypeList(ids), FmsAuxiliaryTypeDO::getId); + } + + /** + * 获得账套的辅助核算类别 Map + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 辅助核算类别 Map + */ + default Map getAuxiliaryTypeMap(Long accountSetId, Long userId) { + return convertMap(getAuxiliaryTypeList(accountSetId, userId), FmsAuxiliaryTypeDO::getId); + } + + /** + * 获得辅助核算类别名称与编号 Map + * + * @param accountSetId 账套编号 + * @return 辅助核算类别名称与编号 Map + */ + default Map getAuxiliaryTypeIdMap(Long accountSetId) { + return convertMap(getAuxiliaryTypeList(accountSetId), + FmsAuxiliaryTypeDO::getName, FmsAuxiliaryTypeDO::getId); + } + + /** + * 校验并获得辅助核算类别列表 + * + * @param ids 辅助核算类别编号数组 + * @return 辅助核算类别列表 + */ + List validateAuxiliaryTypeList(Collection ids); + + /** + * 校验并获得辅助核算类别列表 + * + * @param accountSetId 账套编号 + * @param ids 辅助核算类别编号数组 + * @return 辅助核算类别列表 + */ + List validateAuxiliaryTypeList(Long accountSetId, Collection ids); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeServiceImpl.java new file mode 100644 index 0000000..02a3a61 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeServiceImpl.java @@ -0,0 +1,227 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype.FmsAuxiliaryTypeSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAuxiliaryTypeMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; + +/** + * FMS 辅助核算类别 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsAuxiliaryTypeServiceImpl implements FmsAuxiliaryTypeService { + + @Resource + private FmsAuxiliaryTypeMapper auxiliaryTypeMapper; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private FmsAuxiliaryItemService auxiliaryItemService; + @Resource + private FmsAccountSetService accountSetService; + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private FmsSubjectService subjectService; + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private FmsVoucherService voucherService; + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private FmsVoucherTemplateService voucherTemplateService; + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private FmsInitialBalanceService initialBalanceService; + + @Override + public void initializeDefaultTypes(Long accountSetId) { + List auxiliaryTypes = convertList(FmsAuxiliaryTypeEnum.SYSTEM_PRESET_TYPES, + type -> new FmsAuxiliaryTypeDO().setName(type.getName()).setSystemPreset(true) + .setAccountSetId(accountSetId).setType(type.getType())); + auxiliaryTypeMapper.insertBatch(auxiliaryTypes); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_AUXILIARY_TYPE, subType = FMS_AUXILIARY_TYPE_CREATE_SUB_TYPE, + bizNo = "{{#auxiliaryTypeId}}", success = FMS_AUXILIARY_TYPE_CREATE_SUCCESS) + public Long createAuxiliaryType(FmsAuxiliaryTypeSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验名称唯一 + validateAuxiliaryTypeNameUnique(null, createReqVO.getAccountSetId(), createReqVO.getName()); + + // 2. 创建自定义辅助核算类别 + FmsAuxiliaryTypeDO auxiliaryType = BeanUtils.toBean(createReqVO, FmsAuxiliaryTypeDO.class) + .setId(null).setSystemPreset(false).setType(FmsAuxiliaryTypeEnum.CUSTOM.getType()); + auxiliaryTypeMapper.insert(auxiliaryType); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("auxiliaryTypeId", auxiliaryType.getId()); + return auxiliaryType.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_AUXILIARY_TYPE, subType = FMS_AUXILIARY_TYPE_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_AUXILIARY_TYPE_UPDATE_SUCCESS) + public void updateAuxiliaryType(FmsAuxiliaryTypeSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验辅助核算类别 + FmsAuxiliaryTypeDO auxiliaryType = validateAuxiliaryTypeExists( + updateReqVO.getAccountSetId(), updateReqVO.getId()); + validateCustomAuxiliaryType(auxiliaryType); + // 1.3 校验名称唯一 + validateAuxiliaryTypeNameUnique(auxiliaryType.getId(), updateReqVO.getAccountSetId(), updateReqVO.getName()); + + // 2. 更新辅助核算类别 + auxiliaryTypeMapper.updateById(new FmsAuxiliaryTypeDO() + .setId(auxiliaryType.getId()).setName(updateReqVO.getName())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_AUXILIARY_TYPE, subType = FMS_AUXILIARY_TYPE_DELETE_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_AUXILIARY_TYPE_DELETE_SUCCESS) + public void deleteAuxiliaryType(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验辅助核算类别 + FmsAuxiliaryTypeDO auxiliaryType = validateAuxiliaryTypeExists(accountSetId, id); + validateCustomAuxiliaryType(auxiliaryType); + // 1.3 校验辅助核算类别未被业务引用 + // 1.3.1 校验辅助核算类别下没有项目 + Long auxiliaryItemCount = auxiliaryItemService.getAuxiliaryItemCountByAuxiliaryTypeId(accountSetId, id); + if (auxiliaryItemCount > 0) { + throw exception(AUXILIARY_TYPE_HAS_ITEM, auxiliaryItemCount); + } + // 1.3.2 校验辅助核算类别未被科目使用 + Long subjectCount = subjectService.getSubjectCountByAuxiliaryTypeId(accountSetId, id); + if (subjectCount > 0) { + throw exception(AUXILIARY_TYPE_SUBJECT_IN_USE, subjectCount); + } + // 1.3.3 校验辅助核算类别未被凭证使用 + Long voucherEntryCount = voucherService.getVoucherEntryCountByAuxiliaryTypeId(accountSetId, id); + if (voucherEntryCount > 0) { + throw exception(AUXILIARY_TYPE_VOUCHER_IN_USE, voucherEntryCount); + } + // 1.3.4 校验辅助核算类别未被凭证模板使用 + Long voucherTemplateCount = voucherTemplateService + .getVoucherTemplateCountByAuxiliaryTypeId(accountSetId, id); + if (voucherTemplateCount > 0) { + throw exception(AUXILIARY_TYPE_VOUCHER_TEMPLATE_IN_USE, voucherTemplateCount); + } + // 1.3.5 校验辅助核算类别未被初始余额使用 + Long initialBalanceCount = initialBalanceService.getInitialBalanceCountByAuxiliaryTypeId(accountSetId, id); + if (initialBalanceCount > 0) { + throw exception(AUXILIARY_TYPE_INITIAL_BALANCE_IN_USE, initialBalanceCount); + } + + // 2. 删除辅助核算类别 + auxiliaryTypeMapper.deleteById(id); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("auxiliaryType", auxiliaryType); + } + + @Override + public FmsAuxiliaryTypeDO getAuxiliaryType(Long accountSetId, Long id, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询并校验辅助核算类别 + return validateAuxiliaryTypeExists(accountSetId, id); + } + + @Override + public List getAuxiliaryTypeList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询辅助核算类别 + return auxiliaryTypeMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public List getAuxiliaryTypeList(Long accountSetId) { + return auxiliaryTypeMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public List getAuxiliaryTypeList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return auxiliaryTypeMapper.selectByIds(ids); + } + + @Override + public List validateAuxiliaryTypeList(Collection ids) { + List types = getAuxiliaryTypeList(ids); + if (types.size() != CollUtil.size(ids)) { + throw exception(AUXILIARY_TYPE_NOT_EXISTS); + } + return types; + } + + @Override + public List validateAuxiliaryTypeList(Long accountSetId, Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List types = auxiliaryTypeMapper.selectListByIdsAndAccountSetId(ids, accountSetId); + if (types.size() != CollUtil.distinct(ids).size()) { + throw exception(AUXILIARY_TYPE_NOT_EXISTS); + } + return types; + } + + private FmsAuxiliaryTypeDO validateAuxiliaryTypeExists(Long accountSetId, Long id) { + FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeMapper.selectById(id); + if (auxiliaryType == null || ObjUtil.notEqual(auxiliaryType.getAccountSetId(), accountSetId)) { + throw exception(AUXILIARY_TYPE_NOT_EXISTS); + } + return auxiliaryType; + } + + private void validateAuxiliaryTypeNameUnique(Long id, Long accountSetId, String name) { + FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeMapper.selectByAccountSetIdAndName(accountSetId, name); + if (auxiliaryType != null && ObjUtil.notEqual(auxiliaryType.getId(), id)) { + throw exception(AUXILIARY_TYPE_NAME_DUPLICATE); + } + } + + private void validateCustomAuxiliaryType(FmsAuxiliaryTypeDO auxiliaryType) { + if (Boolean.TRUE.equals(auxiliaryType.getSystemPreset())) { + throw exception(AUXILIARY_TYPE_SYSTEM_NOT_EDITABLE); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyService.java new file mode 100644 index 0000000..4848d53 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyService.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency.FmsCurrencySaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.enums.config.FmsCurrencyPresetEnum; + +import java.util.List; + +/** + * FMS 币别 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsCurrencyService { + + /** + * 初始化本位币 + * + * @param accountSetId 账套编号 + * @param presetCurrency 预置币别 + * @return 本位币 + */ + FmsCurrencyDO initializeStandardCurrency(Long accountSetId, FmsCurrencyPresetEnum presetCurrency); + + /** + * 创建币别 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 币别编号 + */ + Long createCurrency(FmsCurrencySaveReqVO createReqVO, Long userId); + + /** + * 更新币别 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateCurrency(FmsCurrencySaveReqVO updateReqVO, Long userId); + + /** + * 删除币别 + * + * @param accountSetId 账套编号 + * @param id 币别编号 + * @param userId 用户编号 + */ + void deleteCurrency(Long accountSetId, Long id, Long userId); + + /** + * 获得币别列表 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 币别列表 + */ + List getCurrencyList(Long accountSetId, Long userId); + + /** + * 获得币别 + * + * @param accountSetId 账套编号 + * @param id 币别编号 + * @param userId 用户编号 + * @return 币别 + */ + FmsCurrencyDO getCurrency(Long accountSetId, Long id, Long userId); + + /** + * 校验并获得币别列表 + * + * @param accountSetId 账套编号 + * @param ids 币别编号数组 + * @return 币别列表 + */ + List validateCurrencyList(Long accountSetId, List ids); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyServiceImpl.java new file mode 100644 index 0000000..7286bee --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyServiceImpl.java @@ -0,0 +1,174 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency.FmsCurrencySaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsCurrencyMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsCurrencyPresetEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_STANDARD_NOT_DELETABLE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CURRENCY_CREATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CURRENCY_CREATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CURRENCY_DELETE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CURRENCY_DELETE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CURRENCY_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CURRENCY_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CURRENCY_UPDATE_SUCCESS; + +/** + * FMS 币别 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsCurrencyServiceImpl implements FmsCurrencyService { + + @Resource + private FmsCurrencyMapper currencyMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private FmsSubjectService subjectService; + + @Override + public FmsCurrencyDO initializeStandardCurrency(Long accountSetId, FmsCurrencyPresetEnum presetCurrency) { + FmsCurrencyDO currency = new FmsCurrencyDO().setAccountSetId(accountSetId) + .setCode(presetCurrency.getCode()).setName(presetCurrency.getName()) + .setExchangeRate(presetCurrency.getExchangeRate()).setStandard(true); + currencyMapper.insert(currency); + return currency; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CURRENCY_TYPE, subType = FMS_CURRENCY_CREATE_SUB_TYPE, + bizNo = "{{#currencyId}}", success = FMS_CURRENCY_CREATE_SUCCESS) + public Long createCurrency(FmsCurrencySaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验币别编码唯一 + validateCurrencyCodeUnique(null, createReqVO.getAccountSetId(), createReqVO.getCode()); + + // 2. 创建币别 + FmsCurrencyDO currency = BeanUtils.toBean(createReqVO, FmsCurrencyDO.class) + .setId(null).setStandard(false); + currencyMapper.insert(currency); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("currencyId", currency.getId()); + return currency.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CURRENCY_TYPE, subType = FMS_CURRENCY_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_CURRENCY_UPDATE_SUCCESS) + public void updateCurrency(FmsCurrencySaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验币别 + FmsCurrencyDO currency = validateCurrencyExists(updateReqVO.getAccountSetId(), updateReqVO.getId()); + if (Boolean.TRUE.equals(currency.getStandard())) { + updateReqVO.setCode(currency.getCode()); + } + // 1.3 校验币别编码唯一 + validateCurrencyCodeUnique(currency.getId(), updateReqVO.getAccountSetId(), updateReqVO.getCode()); + + // 2. 更新币别 + if (Boolean.TRUE.equals(currency.getStandard())) { + updateReqVO.setExchangeRate(BigDecimal.ONE); + } + currencyMapper.updateById(BeanUtils.toBean(updateReqVO, FmsCurrencyDO.class)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CURRENCY_TYPE, subType = FMS_CURRENCY_DELETE_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_CURRENCY_DELETE_SUCCESS) + public void deleteCurrency(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验币别存在且不是本位币 + FmsCurrencyDO currency = validateCurrencyExists(accountSetId, id); + if (Boolean.TRUE.equals(currency.getStandard())) { + throw exception(CURRENCY_STANDARD_NOT_DELETABLE); + } + // 1.3 校验币别未被科目使用 + if (subjectService.getSubjectCountByCurrencyId(accountSetId, id) > 0) { + throw exception(CURRENCY_IN_USE); + } + + // 2. 删除币别 + currencyMapper.deleteById(id); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("currency", currency); + } + + @Override + public List getCurrencyList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询币别列表 + return currencyMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public FmsCurrencyDO getCurrency(Long accountSetId, Long id, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询并校验币别 + return validateCurrencyExists(accountSetId, id); + } + + @Override + public List validateCurrencyList(Long accountSetId, List ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List currencies = currencyMapper.selectListByIdsAndAccountSetId(ids, accountSetId); + if (currencies.size() != ids.size()) { + throw exception(CURRENCY_NOT_EXISTS); + } + return currencies; + } + + private FmsCurrencyDO validateCurrencyExists(Long accountSetId, Long id) { + FmsCurrencyDO currency = currencyMapper.selectByIdAndAccountSetId(id, accountSetId); + if (currency == null) { + throw exception(CURRENCY_NOT_EXISTS); + } + return currency; + } + + private void validateCurrencyCodeUnique(Long id, Long accountSetId, String code) { + FmsCurrencyDO currency = currencyMapper.selectByAccountSetIdAndCode(accountSetId, code); + if (currency != null && ObjUtil.notEqual(currency.getId(), id)) { + throw exception(CURRENCY_CODE_DUPLICATE); + } + } +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestService.java new file mode 100644 index 0000000..e9dc147 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestService.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest.FmsDigestSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsDigestDO; + +import java.util.List; + +/** + * FMS 常用摘要 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsDigestService { + + /** + * 创建常用摘要 + * + * @param createReqVO 创建信息 + * @param userId 当前用户编号 + * @return 常用摘要编号 + */ + Long createDigest(FmsDigestSaveReqVO createReqVO, Long userId); + + /** + * 修改常用摘要 + * + * @param updateReqVO 修改信息 + * @param userId 当前用户编号 + */ + void updateDigest(FmsDigestSaveReqVO updateReqVO, Long userId); + + /** + * 删除常用摘要 + * + * @param accountSetId 账套编号 + * @param id 常用摘要编号 + * @param userId 当前用户编号 + */ + void deleteDigest(Long accountSetId, Long id, Long userId); + + /** + * 获得账套常用摘要列表 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 常用摘要列表 + */ + List getDigestList(Long accountSetId, Long userId); + + /** + * 校验并获得常用摘要 + * + * @param accountSetId 账套编号 + * @param id 常用摘要编号 + * @return 常用摘要 + */ + FmsDigestDO validateDigestExists(Long accountSetId, Long id); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestServiceImpl.java new file mode 100644 index 0000000..0c0ea5d --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestServiceImpl.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest.FmsDigestSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsDigestDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsDigestMapper; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.DIGEST_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_DIGEST_CREATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_DIGEST_CREATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_DIGEST_DELETE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_DIGEST_DELETE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_DIGEST_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_DIGEST_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_DIGEST_UPDATE_SUCCESS; + +/** + * FMS 常用摘要 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsDigestServiceImpl implements FmsDigestService { + + @Resource + private FmsDigestMapper digestMapper; + + @Resource + private FmsAccountSetService accountSetService; + + @Override + @LogRecord(type = FMS_DIGEST_TYPE, subType = FMS_DIGEST_CREATE_SUB_TYPE, + bizNo = "{{#digestId}}", success = FMS_DIGEST_CREATE_SUCCESS) + public Long createDigest(FmsDigestSaveReqVO createReqVO, Long userId) { + // 1. 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + + // 2. 创建常用摘要 + FmsDigestDO digest = BeanUtils.toBean(createReqVO, FmsDigestDO.class) + .setId(null); + digestMapper.insert(digest); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("digestId", digest.getId()); + return digest.getId(); + } + + @Override + @LogRecord(type = FMS_DIGEST_TYPE, subType = FMS_DIGEST_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_DIGEST_UPDATE_SUCCESS) + public void updateDigest(FmsDigestSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验常用摘要 + validateDigestExists(updateReqVO.getAccountSetId(), updateReqVO.getId()); + + // 2. 更新常用摘要 + digestMapper.updateById(BeanUtils.toBean(updateReqVO, FmsDigestDO.class)); + } + + @Override + @LogRecord(type = FMS_DIGEST_TYPE, subType = FMS_DIGEST_DELETE_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_DIGEST_DELETE_SUCCESS) + public void deleteDigest(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验常用摘要 + FmsDigestDO digest = validateDigestExists(accountSetId, id); + + // 2. 删除常用摘要 + digestMapper.deleteById(id); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("digest", digest); + } + + @Override + public List getDigestList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询常用摘要列表 + return digestMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public FmsDigestDO validateDigestExists(Long accountSetId, Long id) { + FmsDigestDO digest = digestMapper.selectById(id); + if (digest == null || ObjUtil.notEqual(digest.getAccountSetId(), accountSetId)) { + throw exception(DIGEST_NOT_EXISTS); + } + return digest; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceIndicatorService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceIndicatorService.java new file mode 100644 index 0000000..b9277e8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceIndicatorService.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator.FmsFinanceIndicatorSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceIndicatorDO; + +import java.util.List; + +// TODO DONE @AI:Service 接口保留给 Controller、首页和账套初始化调用;方法文档已按 FMS 配置 Service 补齐。 +/** + * FMS 首页财务指标 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsFinanceIndicatorService { + + /** + * 创建首页财务指标 + * + * @param reqVO 创建信息 + * @param userId 当前用户编号 + * @return 指标编号 + */ + Long createFinanceIndicator(FmsFinanceIndicatorSaveReqVO reqVO, Long userId); + + /** + * 更新首页财务指标 + * + * @param reqVO 更新信息 + * @param userId 当前用户编号 + */ + void updateFinanceIndicator(FmsFinanceIndicatorSaveReqVO reqVO, Long userId); + + /** + * 删除首页财务指标 + * + * @param accountSetId 账套编号 + * @param id 指标编号 + * @param userId 当前用户编号 + */ + void deleteFinanceIndicator(Long accountSetId, Long id, Long userId); + + /** + * 获得首页财务指标 + * + * @param accountSetId 账套编号 + * @param id 指标编号 + * @param userId 当前用户编号 + * @return 财务指标 + */ + FmsFinanceIndicatorDO getFinanceIndicator(Long accountSetId, Long id, Long userId); + + /** + * 获得首页财务指标列表 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 财务指标列表 + */ + List getFinanceIndicatorList(Long accountSetId, Long userId); + + /** + * 获得已启用的首页财务指标列表 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 已启用的财务指标列表 + */ + List getEnabledFinanceIndicatorList(Long accountSetId, Long userId); + + /** + * 初始化账套的默认首页财务指标 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + */ + void initializeDefaultFinanceIndicators(Long accountSetId, Long userId); +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceIndicatorServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceIndicatorServiceImpl.java new file mode 100644 index 0000000..12ee561 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceIndicatorServiceImpl.java @@ -0,0 +1,243 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator.FmsFinanceIndicatorSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceIndicatorDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsFinanceIndicatorMapper; +import cn.iocoder.yudao.module.fms.service.report.FmsReportCommonService; +import jakarta.annotation.Resource; +import lombok.Data; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.HOME_METRIC_INVALID; + +/** + * FMS 首页财务指标 Service 实现类 + * + * 指标公式复用报表已有表达式:行次公式使用 L1+L2,科目公式使用报表公式 JSON。 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsFinanceIndicatorServiceImpl implements FmsFinanceIndicatorService { + + // TODO DONE @AI:补充行次公式正则说明。 + /** + * 行次公式格式,例如 {@code L1+L2-L3} + */ + private static final Pattern LINE_FORMULA_PATTERN = Pattern.compile("[+-]?L\\d+(?:[+-]L\\d+)*"); + + private static final String PRESET_RESOURCE = "fms/config/finance-indicator-presets.json"; + + @Resource + private FmsFinanceIndicatorMapper financeIndicatorMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsReportCommonService reportCommonService; + + @Override + public Long createFinanceIndicator(FmsFinanceIndicatorSaveReqVO reqVO, Long userId) { + // TODO DONE @AI:方法步骤已对齐 FMS Service 风格。 + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(reqVO.getAccountSetId(), userId); + // 1.2 校验指标公式 + validateSaveReq(reqVO); + // 1.3 校验指标编码唯一 + validateCodeUnique(reqVO.getAccountSetId(), null, reqVO.getCode()); + + // 2. 创建财务指标 + FmsFinanceIndicatorDO indicator = BeanUtils.toBean(reqVO, FmsFinanceIndicatorDO.class).setId(null) + .setFormula(bindFormula(reqVO.getAccountSetId(), normalizeFormula(reqVO.getFormula()), userId)); + financeIndicatorMapper.insert(indicator); + return indicator.getId(); + } + + @Override + public void updateFinanceIndicator(FmsFinanceIndicatorSaveReqVO reqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(reqVO.getAccountSetId(), userId); + // 1.2 校验财务指标存在 + validateFinanceIndicatorExists(reqVO.getAccountSetId(), reqVO.getId()); + // 1.3 校验指标公式 + validateSaveReq(reqVO); + // 1.4 校验指标编码唯一 + validateCodeUnique(reqVO.getAccountSetId(), reqVO.getId(), reqVO.getCode()); + + // 2. 更新指标 + financeIndicatorMapper.updateById(BeanUtils.toBean(reqVO, FmsFinanceIndicatorDO.class) + .setFormula(bindFormula(reqVO.getAccountSetId(), normalizeFormula(reqVO.getFormula()), userId))); + } + + @Override + public void deleteFinanceIndicator(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验财务指标存在 + validateFinanceIndicatorExists(accountSetId, id); + + // 2. 删除指标 + financeIndicatorMapper.deleteById(id); + } + + @Override + public List getFinanceIndicatorList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 2. 查询首页财务指标 + return financeIndicatorMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public FmsFinanceIndicatorDO getFinanceIndicator(Long accountSetId, Long id, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询并校验财务指标 + return validateFinanceIndicatorExists(accountSetId, id); + } + + @Override + public List getEnabledFinanceIndicatorList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 2. 查询已启用的首页财务指标 + return financeIndicatorMapper.selectListByAccountSetIdAndStatus(accountSetId, + CommonStatusEnum.ENABLE.getStatus()); + } + + private FmsFinanceIndicatorDO validateFinanceIndicatorExists(Long accountSetId, Long id) { + FmsFinanceIndicatorDO indicator = financeIndicatorMapper.selectByIdAndAccountSetId(id, accountSetId); + if (indicator == null) { + throw exception(HOME_METRIC_INVALID); + } + return indicator; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void initializeDefaultFinanceIndicators(Long accountSetId, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验财务指标尚未初始化 + if (CollUtil.isNotEmpty(financeIndicatorMapper.selectListByAccountSetId(accountSetId))) { + return; + } + + // 2. 加载预置配置并绑定当前账套科目 + List presets = loadPresetList(); + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode); + financeIndicatorMapper.insertBatch(convertList(presets, preset -> new FmsFinanceIndicatorDO() + .setAccountSetId(accountSetId).setCode(preset.getCode()).setName(preset.getName()) + .setType(preset.getType()).setFormula(bindFormula(normalizeFormula(preset.getFormula()), subjectMap)) + .setSort(preset.getSort()).setStatus(CommonStatusEnum.ENABLE.getStatus()))); + } + + private void validateSaveReq(FmsFinanceIndicatorSaveReqVO reqVO) { + if (!isValidFormula(reqVO.getFormula())) { + throw exception(HOME_METRIC_INVALID); + } + } + + private boolean isValidFormula(String formula) { + if (StrUtil.isBlank(formula)) { + return false; + } + if (LINE_FORMULA_PATTERN.matcher(formula.replace(" ", "")).matches()) { + return true; + } + try { + return JsonUtils.parseArray(formula, Object.class) != null; + } catch (Exception ignored) { + return false; + } + } + + private String normalizeFormula(String formula) { + String normalized = formula.replace(" ", ""); + return LINE_FORMULA_PATTERN.matcher(normalized).matches() + ? JsonUtils.toJsonString(Collections.singletonList(normalized)) : formula; + } + + private String bindFormula(Long accountSetId, String formula, Long userId) { + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode); + return bindFormula(formula, subjectMap); + } + + private String bindFormula(String formula, Map subjectMap) { + if (StrUtil.isBlank(formula) || reportCommonService.isLineFormula(formula)) { + return formula; + } + return reportCommonService.bindSubjectFormula(formula, subjectMap); + } + + /** + * 加载财务指标预置文件 + * + * @return 财务指标预置配置数组 + */ + private List loadPresetList() { + try (InputStream inputStream = new ClassPathResource(PRESET_RESOURCE).getInputStream()) { + return JsonUtils.parseArray(IoUtil.readUtf8(inputStream), Preset.class); + } catch (Exception exception) { + throw new IllegalStateException("首页财务指标预置文件加载失败", exception); + } + } + + private void validateCodeUnique(Long accountSetId, Long id, String code) { + FmsFinanceIndicatorDO indicator = financeIndicatorMapper.selectByAccountSetIdAndCode(accountSetId, code); + if (indicator != null && ObjUtil.notEqual(indicator.getId(), id)) { + throw exception(HOME_METRIC_INVALID); + } + } + + @Data + private static class Preset { + + /** + * 指标编码 + */ + private String code; + /** + * 指标名称 + */ + private String name; + /** + * 取数报表类型 + */ + private Integer type; + /** + * 指标公式 + */ + private String formula; + /** + * 展示顺序 + */ + private Integer sort; + } +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterService.java new file mode 100644 index 0000000..91d9e9a --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterService.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter.FmsFinanceParameterUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; + +/** + * FMS 财务参数 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsFinanceParameterService { + + /** + * 初始化财务参数 + * + * @param accountSetId 账套编号 + * @param initializeReqVO 账套初始化信息 + */ + void initializeFinanceParameter(Long accountSetId, FmsAccountSetInitializeReqVO initializeReqVO); + + /** + * 获得财务参数 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 财务参数 + */ + FmsFinanceParameterDO getFinanceParameter(Long accountSetId, Long userId); + + /** + * 获得财务参数 + * + * @param accountSetId 账套编号 + * @return 财务参数 + */ + FmsFinanceParameterDO getFinanceParameter(Long accountSetId); + + /** + * 将标准科目编码转换为账套实际科目编码 + * + * @param standardCode 标准科目编码 + * @param subjectCodeRule 科目编码规则 + * @return 账套实际科目编码 + */ + String convertStandardSubjectCode(String standardCode, String subjectCodeRule); + + /** + * 更新财务参数 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateFinanceParameter(FmsFinanceParameterUpdateReqVO updateReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterServiceImpl.java new file mode 100644 index 0000000..7c4cbf2 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterServiceImpl.java @@ -0,0 +1,203 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter.FmsFinanceParameterUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsFinanceParameterMapper; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.service.impl.DiffParseFunction; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_NOT_INITIALIZED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_SETTINGS_RULE_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_SETTINGS_RULE_SHRINK; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.FINANCE_PARAMETER_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_FINANCE_PARAMETER_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_FINANCE_PARAMETER_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_FINANCE_PARAMETER_UPDATE_SUCCESS; + +/** + * FMS 财务参数 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsFinanceParameterServiceImpl implements FmsFinanceParameterService { + + private static final int SUBJECT_CODE_LENGTH_MIN = 2; + private static final int SUBJECT_CODE_LENGTH_MAX = 5; + + @Resource + private FmsFinanceParameterMapper financeParameterMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private FmsSubjectService subjectService; + + @Override + public void initializeFinanceParameter(Long accountSetId, FmsAccountSetInitializeReqVO initializeReqVO) { + // 1. 校验科目层级和编码规则 + List subjectCodeRules = parseSubjectCodeRules(initializeReqVO.getSubjectCodeRule()); + validateSubjectCodeRules(initializeReqVO.getLevel(), subjectCodeRules); + + // 2. 创建财务参数 + FmsFinanceParameterDO financeParameter = new FmsFinanceParameterDO().setAccountSetId(accountSetId) + .setLevel(initializeReqVO.getLevel()).setSubjectCodeRule(initializeReqVO.getSubjectCodeRule()) + .setLedgerBalanceMode(initializeReqVO.getLedgerBalanceMode()) + .setVoucherReviewRequired(FmsFinanceParameterDO.DEFAULT_VOUCHER_REVIEW_REQUIRED) + .setDeficitCheck(FmsFinanceParameterDO.DEFAULT_DEFICIT_CHECK) + .setAssetPeriodLocked(FmsFinanceParameterDO.DEFAULT_ASSET_PERIOD_LOCKED); + financeParameterMapper.insert(financeParameter); + } + + @Override + public FmsFinanceParameterDO getFinanceParameter(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询财务参数 + return getFinanceParameter(accountSetId); + } + + @Override + public FmsFinanceParameterDO getFinanceParameter(Long accountSetId) { + return financeParameterMapper.selectByAccountSetId(accountSetId); + } + + @Override + public String convertStandardSubjectCode(String standardCode, String subjectCodeRule) { + // 1. 解析标准编码规则和账套实际编码规则 + List standardRules = parseSubjectCodeRules(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE); + List actualRules = parseSubjectCodeRules(subjectCodeRule); + // 2. 按科目层级逐段扩展编码 + StringBuilder result = new StringBuilder(); + int offset = 0; + for (int index = 0; index < standardRules.size() && offset < standardCode.length(); index++) { + int standardLength = standardRules.get(index); + int actualLength = actualRules.get(index); + if (offset + standardLength > standardCode.length()) { + throw exception(ACCOUNT_SET_SETTINGS_RULE_INVALID); + } + String segment = standardCode.substring(offset, offset + standardLength); + int increment = actualLength - standardLength; + if (increment > 0 && index == 0) { + result.append(segment.charAt(0)).append(CharSequenceUtil.repeat('0', increment)).append(segment.substring(1)); + } else if (increment > 0) { + result.append(CharSequenceUtil.repeat('0', increment)).append(segment); + } else { + result.append(segment); + } + offset += standardLength; + } + // 3. 标准编码必须由完整的科目层级组成 + if (offset != standardCode.length()) { + throw exception(ACCOUNT_SET_SETTINGS_RULE_INVALID); + } + return result.toString(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_FINANCE_PARAMETER_TYPE, subType = FMS_FINANCE_PARAMETER_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.accountSetId}}", success = FMS_FINANCE_PARAMETER_UPDATE_SUCCESS) + public void updateFinanceParameter(FmsFinanceParameterUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetWritePermission( + updateReqVO.getAccountSetId(), userId); + // 1.2 校验账套初始化状态 + validateAccountSetInitialized(accountSet); + + // 2. 校验财务参数和编码规则 + FmsFinanceParameterDO financeParameter = validateFinanceParameterExists(accountSet.getId()); + List oldRules = parseSubjectCodeRules(financeParameter.getSubjectCodeRule()); + List newRules = parseSubjectCodeRules(updateReqVO.getSubjectCodeRule()); + validateSubjectCodeRules(financeParameter.getLevel(), oldRules, updateReqVO.getLevel(), newRules); + + // 3. 扩展现有科目编码并更新财务参数 + subjectService.expandSubjectCodes(accountSet.getId(), oldRules, newRules); + if (updateReqVO.getStandard() != null && !ObjUtil.equal(accountSet.getStandard(), updateReqVO.getStandard())) { + accountSetService.updateAccountSetStandard(accountSet.getId(), updateReqVO.getStandard(), userId); + } + // 当前页面未维护的领域参数保持原值 + financeParameterMapper.updateById(new FmsFinanceParameterDO().setId(financeParameter.getId()) + .setLevel(updateReqVO.getLevel()).setSubjectCodeRule(updateReqVO.getSubjectCodeRule()) + .setLedgerBalanceMode(updateReqVO.getLedgerBalanceMode()) + .setVoucherReviewRequired(updateReqVO.getVoucherReviewRequired())); + + // 4. 记录操作日志上下文 + LogRecordContext.putVariable("accountSet", accountSet); + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, + BeanUtils.toBean(financeParameter, FmsFinanceParameterUpdateReqVO.class)); + } + + private FmsFinanceParameterDO validateFinanceParameterExists(Long accountSetId) { + FmsFinanceParameterDO financeParameter = financeParameterMapper.selectByAccountSetId(accountSetId); + if (financeParameter == null) { + throw exception(FINANCE_PARAMETER_NOT_EXISTS); + } + return financeParameter; + } + + private void validateAccountSetInitialized(FmsAccountSetDO accountSet) { + if (Boolean.TRUE.equals(accountSet.getInitialized())) { + return; + } + throw exception(ACCOUNT_SET_NOT_INITIALIZED); + } + + private static List parseSubjectCodeRules(String subjectCodeRule) { + return StrUtils.splitToInteger(subjectCodeRule, "-"); + } + + private static void validateSubjectCodeRules(Integer level, List rules) { + if (ObjUtil.notEqual(rules.size(), level) || rules.stream().anyMatch( + rule -> rule < SUBJECT_CODE_LENGTH_MIN || rule > SUBJECT_CODE_LENGTH_MAX)) { + throw exception(ACCOUNT_SET_SETTINGS_RULE_INVALID); + } + } + + /** + * 校验财务参数只扩展科目层级和各级编码长度,避免已使用的科目编码失效 + * + * 每级编码长度限制为 2 至 5 位,与既有财务规则保持一致 + * + * @param oldLevel 原科目层级 + * @param oldRules 原科目编码规则 + * @param newLevel 新科目层级 + * @param newRules 新科目编码规则 + */ + private static void validateSubjectCodeRules(Integer oldLevel, List oldRules, + Integer newLevel, List newRules) { + validateSubjectCodeRules(newLevel, newRules); + if (newLevel < oldLevel || oldRules.size() != oldLevel) { + throw exception(ACCOUNT_SET_SETTINGS_RULE_SHRINK); + } + for (int index = 0; index < oldRules.size(); index++) { + if (newRules.get(index) < oldRules.get(index)) { + throw exception(ACCOUNT_SET_SETTINGS_RULE_SHRINK); + } + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceService.java new file mode 100644 index 0000000..3eb1a00 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceService.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO; + +import java.util.Collection; +import java.util.List; + +/** + * FMS 初始余额 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsInitialBalanceService { + + /** + * 获得初始余额列表 + * + * 返回按科目层级展开的平铺列表,父级科目排在子级之前,父级余额由末级科目逐级汇总 + * + * @param accountSetId 账套编号 + * @param subjectType 科目类别 + * @param userId 用户编号 + * @return 初始余额列表 + */ + List getInitialBalanceList(Long accountSetId, Integer subjectType, Long userId); + + /** + * 保存初始余额 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void saveInitialBalance(FmsInitialBalanceSaveReqVO saveReqVO, Long userId); + + /** + * 获得试算平衡结果 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 试算平衡结果 + */ + FmsTrialBalanceRespVO getTrialBalance(Long accountSetId, Long userId); + + /** + * 导入初始余额 + * + * @param accountSetId 账套编号 + * @param rows Excel 数据 + * @param userId 用户编号 + * @return 导入数量 + */ + int importInitialBalance(Long accountSetId, List rows, Long userId); + + /** + * 获得指定科目的初始余额数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号数组 + * @return 初始余额数量 + */ + Long getInitialBalanceCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 获得指定科目包含数量数据的初始余额数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号数组 + * @return 初始余额数量 + */ + Long getInitialBalanceQuantityCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 迁移科目的初始余额 + * + * @param accountSetId 账套编号 + * @param subjectId 科目编号 + * @param targetSubjectId 目标科目编号 + */ + void updateInitialBalanceSubject(Long accountSetId, Long subjectId, Long targetSubjectId); + + /** + * 获得引用指定辅助核算项目的初始余额数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryItemIds 辅助核算项目编号数组 + * @return 初始余额数量 + */ + Long getInitialBalanceCountByAuxiliaryItemIds(Long accountSetId, Collection auxiliaryItemIds); + + /** + * 获得引用指定辅助核算类别的初始余额数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @return 初始余额数量 + */ + Long getInitialBalanceCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceServiceImpl.java new file mode 100644 index 0000000..62dda3e --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceServiceImpl.java @@ -0,0 +1,725 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsInitialBalanceDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsInitialBalanceMapper; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sumBigDecimal; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_AUXILIARY_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_IMPORT_ROW_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_PERIOD_CLOSED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_PROFIT_LOSS_YEAR_OPENING_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_SUBJECT_NOT_LEAF; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; + +/** + * FMS 初始余额 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsInitialBalanceServiceImpl implements FmsInitialBalanceService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + @Resource + private FmsInitialBalanceMapper initialBalanceMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsAuxiliaryItemService auxiliaryItemService; + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + @Resource + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + @Resource + @Lazy // 延迟加载,避免与结账 Service 循环依赖 + private FmsClosingPeriodService closingPeriodService; + + @Override + public List getInitialBalanceList(Long accountSetId, Integer subjectType, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 加载科目、辅助核算类别和科目余额 + List subjects = subjectService.getSubjectList(accountSetId, subjectType, userId); + Map auxiliaryTypeMap = getAuxiliaryTypeMap(accountSetId, subjects); + Map balanceMap = convertMap( + initialBalanceMapper.selectListByAccountSetId(accountSetId), FmsInitialBalanceDO::getSubjectId); + + // 2.1 构建平铺列表,父级科目按编码排在子级之前 + Map balanceVOMap = new LinkedHashMap<>(); + subjects.forEach(subject -> balanceVOMap.put(subject.getId(), + buildInitialBalanceRespVO(subject, balanceMap.get(subject.getId()), auxiliaryTypeMap))); + List list = new ArrayList<>(balanceVOMap.values()); + list.sort(Comparator.comparing(FmsInitialBalanceRespVO::getSubjectCode) + .thenComparing(FmsInitialBalanceRespVO::getSubjectId)); + // 2.2 父级余额由末级科目逐级汇总 + aggregateParentBalances(list, balanceVOMap); + return list; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_INITIAL_BALANCE_TYPE, subType = FMS_INITIAL_BALANCE_SAVE_SUB_TYPE, + bizNo = "{{#saveReqVO.accountSetId}}", success = FMS_INITIAL_BALANCE_SAVE_SUCCESS) + public void saveInitialBalance(FmsInitialBalanceSaveReqVO saveReqVO, Long userId) { + // 1. 校验账套写权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetWritePermission( + saveReqVO.getAccountSetId(), userId); + + // 2. 保存财务初始余额 + saveInitialBalance(saveReqVO, userId, accountSet); + } + + /** + * 保存财务初始余额 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @param accountSet 账套 + */ + private void saveInitialBalance(FmsInitialBalanceSaveReqVO saveReqVO, Long userId, FmsAccountSetDO accountSet) { + // 1.1 校验结账状态 + validateAccountSetEditable(accountSet); + // 1.2 加载科目和辅助核算类别 + List subjects = subjectService.getSubjectList(saveReqVO.getAccountSetId(), null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + Map auxiliaryTypeMap = getAuxiliaryTypeMap(saveReqVO.getAccountSetId(), subjects); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId); + // 1.3 校验科目余额 + for (FmsInitialBalanceSaveReqVO.Balance reqVO : saveReqVO.getBalances()) { + validateSaveBalance(subjectMap.get(reqVO.getSubjectId()), parentIds, reqVO); + } + + // 2. 构建科目余额 + List balances = convertList(saveReqVO.getBalances(), + reqVO -> buildBalance(saveReqVO.getAccountSetId(), subjectMap.get(reqVO.getSubjectId()), + reqVO, auxiliaryTypeMap)); + + // 3.1 批量查询已有科目余额 + Map existingMap = convertMap( + initialBalanceMapper.selectListByAccountSetIdAndSubjectIds(saveReqVO.getAccountSetId(), + convertList(balances, FmsInitialBalanceDO::getSubjectId)), FmsInitialBalanceDO::getSubjectId); + // 3.2 拆分新增和更新的科目余额 + List insertBalances = new ArrayList<>(); + List updateBalances = new ArrayList<>(); + balances.forEach(balance -> { + FmsInitialBalanceDO existing = existingMap.get(balance.getSubjectId()); + if (existing == null) { + insertBalances.add(balance); + } else { + balance.setId(existing.getId()); + updateBalances.add(balance); + } + }); + // 3.3 批量新增或更新科目余额 + if (CollUtil.isNotEmpty(insertBalances)) { + initialBalanceMapper.insertBatch(insertBalances); + } + if (CollUtil.isNotEmpty(updateBalances)) { + initialBalanceMapper.updateBatch(updateBalances); + } + } + + @Override + public FmsTrialBalanceRespVO getTrialBalance(Long accountSetId, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 加载末级科目余额 + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + List balances = filterList(initialBalanceMapper.selectListByAccountSetId(accountSetId), + balance -> !parentIds.contains(balance.getSubjectId()) + && subjectMap.containsKey(balance.getSubjectId())); + + // 2.1 汇总期初和本年累计的借贷金额 + BigDecimal openingDebitAmount = sumByDirection(balances, subjectMap, + FmsDebitCreditDirectionEnum.DEBIT.getType(), FmsInitialBalanceDO::getOpeningAmount); + BigDecimal openingCreditAmount = sumByDirection(balances, subjectMap, + FmsDebitCreditDirectionEnum.CREDIT.getType(), FmsInitialBalanceDO::getOpeningAmount); + BigDecimal yearDebitAmount = sumBigDecimal(balances, FmsInitialBalanceDO::getYearDebitAmount); + BigDecimal yearCreditAmount = sumBigDecimal(balances, FmsInitialBalanceDO::getYearCreditAmount); + BigDecimal openingDifferenceAmount = openingDebitAmount.subtract(openingCreditAmount); + BigDecimal yearDifferenceAmount = yearDebitAmount.subtract(yearCreditAmount); + // 2.2 构建试算平衡结果 + return new FmsTrialBalanceRespVO().setOpeningDebitAmount(openingDebitAmount) + .setOpeningCreditAmount(openingCreditAmount).setOpeningDifferenceAmount(openingDifferenceAmount) + .setYearDebitAmount(yearDebitAmount).setYearCreditAmount(yearCreditAmount) + .setYearDifferenceAmount(yearDifferenceAmount) + .setBalanced(openingDifferenceAmount.signum() == 0 && yearDifferenceAmount.signum() == 0); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_INITIAL_BALANCE_TYPE, subType = FMS_INITIAL_BALANCE_IMPORT_SUB_TYPE, + bizNo = "{{#accountSetId}}", success = FMS_INITIAL_BALANCE_IMPORT_SUCCESS) + public int importInitialBalance(Long accountSetId, + List rows, Long userId) { + // 1.1 校验账套写权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验结账状态 + validateAccountSetEditable(accountSet); + // 1.3 加载科目和辅助核算项目 + boolean january = LocalDateTimeUtils.isJanuary(accountSet.getStartTime()); + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode); + Set parentIds = convertSet(subjects, FmsSubjectDO::getParentId); + List auxiliaryTypes = auxiliaryTypeService + .getAuxiliaryTypeList(accountSetId, userId); + Map auxiliaryTypeMap = convertMap(auxiliaryTypes, FmsAuxiliaryTypeDO::getName); + Map auxiliaryTypeIdMap = convertMap( + auxiliaryTypes, FmsAuxiliaryTypeDO::getId); + List auxiliaryItems = auxiliaryItemService + .getAuxiliaryItemListByAccountSetId(accountSetId, userId); + Map> auxiliaryItemMap = auxiliaryItems.stream() + .collect(Collectors.groupingBy(FmsAuxiliaryItemDO::getAuxiliaryTypeId)); + + // 2. 校验并转换导入数据 + Map balanceMap = new LinkedHashMap<>(); + Set rowKeys = new HashSet<>(); + for (FmsInitialBalanceExcelVO row : rows) { + // 2.1 校验科目和金额,辅助核算科目的全零明细行直接跳过 + FmsSubjectDO subject = subjectMap.get(row.getSubjectCode()); + validateImportSubject(row, subject, parentIds); + if (CollUtil.isNotEmpty(subject.getAuxiliaryTypeIds()) + && StrUtil.isBlank(row.getAuxiliaryItems()) && isZeroRow(row)) { + continue; + } + // 2.2 同一科目和辅助核算项目组合只允许出现一次 + String rowKey = subject.getId() + "#" + row.getAuxiliaryItems(); + if (!rowKeys.add(rowKey)) { + throw importRowException(row, "科目和辅助核算项目重复"); + } + // 2.3 未启用辅助核算的科目直接复制金额,启用时按辅助核算项目逐行转换 + FmsInitialBalanceSaveReqVO.Balance balance = balanceMap.computeIfAbsent(subject.getId(), + key -> new FmsInitialBalanceSaveReqVO.Balance().setSubjectId(subject.getId()) + .setAssistBalances(new ArrayList<>())); + if (CollUtil.isEmpty(subject.getAuxiliaryTypeIds())) { + if (StrUtil.isNotBlank(row.getAuxiliaryItems())) { + throw importRowException(row, "该科目未启用辅助核算"); + } + copyAmounts(row, balance, subject, january); + continue; + } + FmsInitialBalanceSaveReqVO.AssistBalance assistBalance = new FmsInitialBalanceSaveReqVO.AssistBalance() + .setAuxiliaryItemIds(parseAuxiliaryItemIds(row, subject, auxiliaryTypeMap, auxiliaryTypeIdMap, auxiliaryItemMap)); + copyAmounts(row, assistBalance, subject, january); + balance.getAssistBalances().add(assistBalance); + } + if (MapUtil.isEmpty(balanceMap)) { + return 0; + } + + // 3. 复用页面保存校验和持久化逻辑 + FmsInitialBalanceSaveReqVO saveReqVO = new FmsInitialBalanceSaveReqVO() + .setAccountSetId(accountSetId).setBalances(new ArrayList<>(balanceMap.values())); + saveInitialBalance(saveReqVO, userId, accountSet); + return balanceMap.size(); + } + + @Override + public Long getInitialBalanceCountBySubjectIds(Long accountSetId, Collection subjectIds) { + return initialBalanceMapper.selectCountByAccountSetIdAndSubjectIds(accountSetId, subjectIds); + } + + @Override + public Long getInitialBalanceQuantityCountBySubjectIds(Long accountSetId, Collection subjectIds) { + List balances = initialBalanceMapper.selectListByAccountSetIdAndSubjectIds( + accountSetId, subjectIds); + return (long) CollUtil.count(balances, this::hasQuantityData); + } + + @Override + public void updateInitialBalanceSubject(Long accountSetId, Long subjectId, Long targetSubjectId) { + initialBalanceMapper.updateSubject(accountSetId, subjectId, + new FmsInitialBalanceDO().setSubjectId(targetSubjectId)); + } + + @Override + public Long getInitialBalanceCountByAuxiliaryItemIds( + Long accountSetId, Collection auxiliaryItemIds) { + if (CollUtil.isEmpty(auxiliaryItemIds)) { + return 0L; + } + Set auxiliaryItemIdSet = new HashSet<>(auxiliaryItemIds); + return countInitialBalancesByAuxiliary(accountSetId, + item -> auxiliaryItemIdSet.contains(item.getItemId())); + } + + @Override + public Long getInitialBalanceCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId) { + return countInitialBalancesByAuxiliary(accountSetId, + item -> ObjUtil.equal(item.getTypeId(), auxiliaryTypeId)); + } + + /** + * 统计辅助核算项目满足条件的初始余额数量 + */ + private Long countInitialBalancesByAuxiliary(Long accountSetId, + Predicate predicate) { + List balances = initialBalanceMapper.selectListByAccountSetId(accountSetId); + // 展开每条初始余额的辅助核算项,任一辅助核算命中条件即计入 + return (long) CollUtil.count(balances, balance -> CollUtil.isNotEmpty(balance.getAssistBalances()) + && balance.getAssistBalances().stream() + .filter(assistBalance -> CollUtil.isNotEmpty(assistBalance.getAuxiliaries())) + .flatMap(assistBalance -> assistBalance.getAuxiliaries().stream()) + .anyMatch(predicate)); + } + + /** + * 判断初始余额是否包含数量数据 + * + * @param balance 初始余额 + * @return 是否包含数量数据 + */ + private boolean hasQuantityData(FmsInitialBalanceDO balance) { + // 科目余额存在实际数量 + if (hasQuantityData(balance.getOpeningQuantity(), balance.getYearDebitQuantity(), + balance.getYearCreditQuantity(), balance.getYearOpeningQuantity(), + balance.getProfitLossQuantity())) { + return true; + } + // 辅助核算余额存在实际数量 + return CollUtil.isNotEmpty(balance.getAssistBalances()) + && CollUtil.findOne(balance.getAssistBalances(), assistBalance -> hasQuantityData( + assistBalance.getOpeningQuantity(), assistBalance.getYearDebitQuantity(), + assistBalance.getYearCreditQuantity(), assistBalance.getYearOpeningQuantity(), + assistBalance.getProfitLossQuantity())) != null; + } + + private boolean hasQuantityData(BigDecimal... quantities) { + return Stream.of(quantities).anyMatch(quantity -> NumberUtils.zeroIfNull(quantity).signum() != 0); + } + + /** + * 校验账套未结账,结账后初始余额不允许修改或导入 + */ + private void validateAccountSetEditable(FmsAccountSetDO accountSet) { + if (ObjUtil.notEqual(closingPeriodService.getCurrentMonth(accountSet.getId(), + accountSet.getStartTime()), YearMonth.from(accountSet.getStartTime()))) { + throw exception(INITIAL_BALANCE_PERIOD_CLOSED); + } + } + + /** + * 校验科目余额:科目必须存在且为末级,损益类科目的年初余额必须为 0 + */ + private void validateSaveBalance(FmsSubjectDO subject, Set parentIds, + FmsInitialBalanceSaveReqVO.Balance reqVO) { + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + if (parentIds.contains(subject.getId())) { + throw exception(INITIAL_BALANCE_SUBJECT_NOT_LEAF); + } + validateProfitLossBalance(subject, reqVO.getYearOpeningAmount()); + } + + /** + * 汇总父级科目余额:非末级科目先清零,再按“方向相同取正、相反取负”自底向上累加子级 + */ + private void aggregateParentBalances(List list, + Map balanceVOMap) { + // 1. 非末级科目的余额由子级汇总,先清零 + Set parentIds = convertSet(list, FmsInitialBalanceRespVO::getParentId); + list.forEach(balanceVO -> { + if (parentIds.contains(balanceVO.getSubjectId())) { + setZeroAmounts(balanceVO); + } + }); + // 2. 平铺列表父级在前,倒序遍历时子级先完成汇总,再逐级累加到父级 + for (int index = list.size() - 1; index >= 0; index--) { + FmsInitialBalanceRespVO balanceVO = list.get(index); + FmsInitialBalanceRespVO parent = balanceVOMap.get(balanceVO.getParentId()); + if (parent == null) { + continue; + } + accumulateParentBalance(parent, balanceVO); + } + } + + /** + * 将子级科目余额累加到父级:期初、年初和损益发生额均按余额方向取符号,累计发生额按借贷分别累加 + */ + private void accumulateParentBalance(FmsInitialBalanceRespVO parent, FmsInitialBalanceRespVO child) { + parent.setOpeningAmount(parent.getOpeningAmount().add(directional(child.getOpeningAmount(), child, parent))); + parent.setOpeningQuantity(parent.getOpeningQuantity().add(directional(child.getOpeningQuantity(), child, parent))); + parent.setYearDebitAmount(parent.getYearDebitAmount().add(child.getYearDebitAmount())); + parent.setYearDebitQuantity(parent.getYearDebitQuantity().add(child.getYearDebitQuantity())); + parent.setYearCreditAmount(parent.getYearCreditAmount().add(child.getYearCreditAmount())); + parent.setYearCreditQuantity(parent.getYearCreditQuantity().add(child.getYearCreditQuantity())); + parent.setYearOpeningAmount(parent.getYearOpeningAmount().add(directional(child.getYearOpeningAmount(), child, parent))); + parent.setYearOpeningQuantity(parent.getYearOpeningQuantity().add(directional(child.getYearOpeningQuantity(), child, parent))); + parent.setProfitLossAmount(parent.getProfitLossAmount().add( + directional(child.getProfitLossAmount(), child, parent))); + parent.setProfitLossQuantity(parent.getProfitLossQuantity().add( + directional(child.getProfitLossQuantity(), child, parent))); + } + + /** + * 子级与父级余额方向相同时取正数,相反时取负数 + */ + private BigDecimal directional(BigDecimal amount, FmsInitialBalanceRespVO child, FmsInitialBalanceRespVO parent) { + return ObjUtil.equal(child.getBalanceDirection(), parent.getBalanceDirection()) ? amount : amount.negate(); + } + + /** + * 构建科目余额,启用辅助核算时科目金额由辅助核算明细汇总 + */ + private FmsInitialBalanceDO buildBalance(Long accountSetId, FmsSubjectDO subject, + FmsInitialBalanceSaveReqVO.Balance reqVO, + Map auxiliaryTypeMap) { + boolean auxiliaryAccounting = CollUtil.isNotEmpty(subject.getAuxiliaryTypeIds()); + FmsInitialBalanceDO balance = new FmsInitialBalanceDO().setSubjectId(subject.getId()) + .setAccountSetId(accountSetId).setAuxiliaryAccounting(auxiliaryAccounting) + .setAssistBalances(Collections.emptyList()); + if (auxiliaryAccounting) { + List assistBalances = buildAssistBalances( + accountSetId, subject, reqVO.getAssistBalances(), auxiliaryTypeMap); + balance.setAssistBalances(assistBalances); + sumAssistBalances(balance, assistBalances); + return balance; + } + copyAmounts(reqVO, balance); + return balance; + } + + /** + * 构建辅助核算余额明细:逐项校验项目与科目配置,并获取或创建辅助核算组合 + */ + private List buildAssistBalances(Long accountSetId, + FmsSubjectDO subject, List reqVOs, + Map auxiliaryTypeMap) { + if (CollUtil.isEmpty(reqVOs)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + Set combinationIds = new HashSet<>(); + for (FmsInitialBalanceSaveReqVO.AssistBalance reqVO : reqVOs) { + // 1. 校验辅助核算项目与科目启用的类别一致 + List itemIds = new ArrayList<>(new LinkedHashSet<>(reqVO.getAuxiliaryItemIds())); + List items = auxiliaryItemService.validateAuxiliaryItemList(accountSetId, itemIds); + Map itemMap = convertMap(items, FmsAuxiliaryItemDO::getAuxiliaryTypeId); + if (ObjUtil.notEqual(itemMap.size(), subject.getAuxiliaryTypeIds().size()) + || subject.getAuxiliaryTypeIds().stream().anyMatch( + auxiliaryTypeId -> !itemMap.containsKey(auxiliaryTypeId))) { + throw exception(INITIAL_BALANCE_AUXILIARY_INVALID); + } + // 2. 按科目启用的类别顺序,构建辅助核算组合明细 + List combinationItems = new ArrayList<>(); + List auxiliaryItems = new ArrayList<>(); + subject.getAuxiliaryTypeIds().forEach(auxiliaryTypeId -> { + FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeMap.get(auxiliaryTypeId); + FmsAuxiliaryItemDO item = itemMap.get(auxiliaryTypeId); + combinationItems.add(FmsAuxiliaryCombinationDO.AuxiliaryItem.builder() + .type(auxiliaryType.getType()).typeId(auxiliaryTypeId) + .itemId(item.getId()).name(item.getName()).build()); + auxiliaryItems.add(FmsInitialBalanceDO.AuxiliaryItem.builder() + .type(auxiliaryType.getType()).typeId(auxiliaryTypeId) + .itemId(item.getId()).name(item.getName()).build()); + }); + // 3. 保存辅助核算组合,同一科目下同一组合只允许一条明细 + FmsAuxiliaryCombinationDO combination = auxiliaryCombinationService.saveAuxiliaryCombination( + accountSetId, subject.getId(), combinationItems); + if (!combinationIds.add(combination.getId())) { + throw exception(INITIAL_BALANCE_AUXILIARY_INVALID); + } + FmsInitialBalanceDO.AssistBalance assist = new FmsInitialBalanceDO.AssistBalance() + .setAssistCombinationId(combination.getId()).setAuxiliaries(auxiliaryItems); + copyAmounts(reqVO, assist); + validateProfitLossBalance(subject, assist.getYearOpeningAmount()); + result.add(assist); + } + return result; + } + + /** + * 构建科目初始余额响应,无余额记录时金额按 0 返回 + */ + private FmsInitialBalanceRespVO buildInitialBalanceRespVO(FmsSubjectDO subject, + FmsInitialBalanceDO balance, Map auxiliaryTypeMap) { + FmsInitialBalanceRespVO respVO = new FmsInitialBalanceRespVO().setSubjectId(subject.getId()) + .setSubjectCode(subject.getCode()).setSubjectName(subject.getName()) + .setParentId(subject.getParentId()).setType(subject.getType()) + .setBalanceDirection(subject.getBalanceDirection()) + .setQuantityAccounting(subject.getQuantityAccounting()).setQuantityUnit(subject.getQuantityUnit()) + .setAuxiliaryAccounting(CollUtil.isNotEmpty(subject.getAuxiliaryTypeIds())) + .setAuxiliaryConfigs(convertList(subject.getAuxiliaryTypeIds(), auxiliaryTypeId -> { + FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeMap.get(auxiliaryTypeId); + return new FmsInitialBalanceRespVO.AuxiliaryConfig().setAuxiliaryTypeId(auxiliaryTypeId) + .setType(auxiliaryType.getType()).setName(auxiliaryType.getName()); + })); + if (balance == null) { + setZeroAmounts(respVO); + respVO.setAssistBalances(Collections.emptyList()); + return respVO; + } + respVO.setId(balance.getId()); + copyAmounts(balance, respVO); + if (CollUtil.isEmpty(balance.getAssistBalances())) { + respVO.setAssistBalances(Collections.emptyList()); + } else { + respVO.setAssistBalances(convertList(balance.getAssistBalances(), assist -> { + FmsInitialBalanceRespVO.AssistBalance assistVO = + BeanUtils.toBean(assist, FmsInitialBalanceRespVO.AssistBalance.class); + assistVO.setAuxiliaries(convertList(assist.getAuxiliaries(), + item -> BeanUtils.toBean(item, FmsInitialBalanceRespVO.AuxiliaryItem.class))); + return assistVO; + })); + } + return respVO; + } + + private void copyAmounts(FmsInitialBalanceDO source, FmsInitialBalanceRespVO target) { + target.setOpeningAmount(NumberUtils.zeroIfNull(source.getOpeningAmount())) + .setOpeningQuantity(NumberUtils.zeroIfNull(source.getOpeningQuantity())) + .setYearDebitAmount(NumberUtils.zeroIfNull(source.getYearDebitAmount())) + .setYearDebitQuantity(NumberUtils.zeroIfNull(source.getYearDebitQuantity())) + .setYearCreditAmount(NumberUtils.zeroIfNull(source.getYearCreditAmount())) + .setYearCreditQuantity(NumberUtils.zeroIfNull(source.getYearCreditQuantity())) + .setYearOpeningAmount(NumberUtils.zeroIfNull(source.getYearOpeningAmount())) + .setYearOpeningQuantity(NumberUtils.zeroIfNull(source.getYearOpeningQuantity())) + .setProfitLossAmount(NumberUtils.zeroIfNull(source.getProfitLossAmount())) + .setProfitLossQuantity(NumberUtils.zeroIfNull(source.getProfitLossQuantity())); + } + + /** + * 汇总指定余额方向的金额 + */ + private BigDecimal sumByDirection(List balances, + Map subjectMap, Integer direction, + Function getter) { + List directionBalances = filterList(balances, balance -> ObjUtil.equal( + subjectMap.get(balance.getSubjectId()).getBalanceDirection(), direction)); + return sumBigDecimal(directionBalances, getter); + } + + private void copyAmounts(FmsInitialBalanceSaveReqVO.Amounts source, FmsInitialBalanceDO target) { + target.setOpeningAmount(NumberUtils.zeroIfNull(source.getOpeningAmount())) + .setOpeningQuantity(NumberUtils.zeroIfNull(source.getOpeningQuantity())) + .setYearDebitAmount(NumberUtils.zeroIfNull(source.getYearDebitAmount())) + .setYearDebitQuantity(NumberUtils.zeroIfNull(source.getYearDebitQuantity())) + .setYearCreditAmount(NumberUtils.zeroIfNull(source.getYearCreditAmount())) + .setYearCreditQuantity(NumberUtils.zeroIfNull(source.getYearCreditQuantity())) + .setYearOpeningAmount(NumberUtils.zeroIfNull(source.getYearOpeningAmount())) + .setYearOpeningQuantity(NumberUtils.zeroIfNull(source.getYearOpeningQuantity())) + .setProfitLossAmount(NumberUtils.zeroIfNull(source.getProfitLossAmount())) + .setProfitLossQuantity(NumberUtils.zeroIfNull(source.getProfitLossQuantity())); + } + + private void copyAmounts(FmsInitialBalanceSaveReqVO.Amounts source, + FmsInitialBalanceDO.AssistBalance target) { + target.setOpeningAmount(NumberUtils.zeroIfNull(source.getOpeningAmount())) + .setOpeningQuantity(NumberUtils.zeroIfNull(source.getOpeningQuantity())) + .setYearDebitAmount(NumberUtils.zeroIfNull(source.getYearDebitAmount())) + .setYearDebitQuantity(NumberUtils.zeroIfNull(source.getYearDebitQuantity())) + .setYearCreditAmount(NumberUtils.zeroIfNull(source.getYearCreditAmount())) + .setYearCreditQuantity(NumberUtils.zeroIfNull(source.getYearCreditQuantity())) + .setYearOpeningAmount(NumberUtils.zeroIfNull(source.getYearOpeningAmount())) + .setYearOpeningQuantity(NumberUtils.zeroIfNull(source.getYearOpeningQuantity())) + .setProfitLossAmount(NumberUtils.zeroIfNull(source.getProfitLossAmount())) + .setProfitLossQuantity(NumberUtils.zeroIfNull(source.getProfitLossQuantity())); + } + + private void sumAssistBalances(FmsInitialBalanceDO balance, + List assists) { + balance.setOpeningAmount(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getOpeningAmount)) + .setOpeningQuantity(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getOpeningQuantity)) + .setYearDebitAmount(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getYearDebitAmount)) + .setYearDebitQuantity(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getYearDebitQuantity)) + .setYearCreditAmount(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getYearCreditAmount)) + .setYearCreditQuantity(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getYearCreditQuantity)) + .setYearOpeningAmount(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getYearOpeningAmount)) + .setYearOpeningQuantity(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getYearOpeningQuantity)) + .setProfitLossAmount(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getProfitLossAmount)) + .setProfitLossQuantity(sumBigDecimal(assists, FmsInitialBalanceDO.AssistBalance::getProfitLossQuantity)); + } + + private void validateImportSubject(FmsInitialBalanceExcelVO row, FmsSubjectDO subject, + Set parentIds) { + if (StrUtil.isBlank(row.getSubjectCode())) { + throw importRowException(row, "科目编码不能为空"); + } + if (subject == null) { + throw importRowException(row, "科目编码不存在"); + } + if (parentIds.contains(subject.getId())) { + throw importRowException(row, "只能导入末级科目"); + } + FmsDebitCreditDirectionEnum direction = FmsDebitCreditDirectionEnum + .valueOf(subject.getBalanceDirection()); + if (StrUtil.isNotBlank(row.getDirectionName()) + && !StrUtil.equals(direction.getName(), row.getDirectionName())) { + throw importRowException(row, "余额方向与科目不一致"); + } + validateAmounts(row); + } + + private void validateAmounts(FmsInitialBalanceExcelVO row) { + List amounts = Arrays.asList(row.getOpeningQuantity(), row.getOpeningAmount(), + row.getYearDebitQuantity(), row.getYearDebitAmount(), row.getYearCreditQuantity(), + row.getYearCreditAmount(), row.getProfitLossQuantity(), row.getProfitLossAmount()); + if (amounts.stream().filter(ObjUtil::isNotNull).anyMatch(amount -> amount.signum() < 0)) { + throw importRowException(row, "数量和金额不能小于 0"); + } + } + + /** + * 解析导入行的辅助核算项目文本,按科目启用的类别顺序返回项目编号 + */ + private List parseAuxiliaryItemIds(FmsInitialBalanceExcelVO row, + FmsSubjectDO subject, Map auxiliaryTypeMap, + Map auxiliaryTypeIdMap, + Map> auxiliaryItemMap) { + // 1. 按“类别:名称/类别:名称”解析文本,类别不允许重复 + if (StrUtil.isBlank(row.getAuxiliaryItems())) { + throw importRowException(row, "辅助核算项目不能为空"); + } + Map values = new LinkedHashMap<>(); + for (String segment : StrUtil.split(row.getAuxiliaryItems(), '/')) { + List parts = StrUtil.split(segment, ':'); + if (parts.size() != 2 || StrUtil.isBlank(parts.get(0)) || StrUtil.isBlank(parts.get(1))) { + throw importRowException(row, "辅助核算项目格式不正确"); + } + FmsAuxiliaryTypeDO type = auxiliaryTypeMap.get(parts.get(0)); + if (type == null || values.put(type.getId(), parts.get(1)) != null) { + throw importRowException(row, "辅助核算类别不存在或重复"); + } + } + // 2. 按科目启用的类别匹配辅助核算项目,名称或编码均可 + List itemIds = new ArrayList<>(); + for (Long auxiliaryTypeId : subject.getAuxiliaryTypeIds()) { + String itemValue = values.get(auxiliaryTypeId); + if (StrUtil.isBlank(itemValue)) { + throw importRowException(row, "辅助核算项目与科目配置不一致"); + } + FmsAuxiliaryItemDO item = CollUtil.findOne( + auxiliaryItemMap.getOrDefault(auxiliaryTypeId, Collections.emptyList()), + candidate -> StrUtil.equals(candidate.getName(), itemValue) + || StrUtil.equals(candidate.getCode(), itemValue)); + if (item == null) { + throw importRowException(row, auxiliaryTypeIdMap.get(auxiliaryTypeId).getName() + "项目不存在"); + } + itemIds.add(item.getId()); + } + // 3. 校验类别数量与科目配置一致 + if (values.size() != subject.getAuxiliaryTypeIds().size()) { + throw importRowException(row, "辅助核算项目与科目配置不一致"); + } + return itemIds; + } + + private Map getAuxiliaryTypeMap( + Long accountSetId, List subjects) { + List list = auxiliaryTypeService.validateAuxiliaryTypeList(accountSetId, + convertSetByFlatMap(subjects, FmsSubjectDO::getAuxiliaryTypeIds, List::stream)); + return convertMap(list, FmsAuxiliaryTypeDO::getId); + } + + private void copyAmounts(FmsInitialBalanceExcelVO source, + FmsInitialBalanceSaveReqVO.Amounts target, FmsSubjectDO subject, boolean january) { + BigDecimal openingAmount = NumberUtils.zeroIfNull(source.getOpeningAmount()); + BigDecimal openingQuantity = NumberUtils.zeroIfNull(source.getOpeningQuantity()); + BigDecimal yearDebitAmount = january ? ZERO : NumberUtils.zeroIfNull(source.getYearDebitAmount()); + BigDecimal yearDebitQuantity = january ? ZERO : NumberUtils.zeroIfNull(source.getYearDebitQuantity()); + BigDecimal yearCreditAmount = january ? ZERO : NumberUtils.zeroIfNull(source.getYearCreditAmount()); + BigDecimal yearCreditQuantity = january ? ZERO : NumberUtils.zeroIfNull(source.getYearCreditQuantity()); + boolean debit = ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()); + BigDecimal yearOpeningAmount = january ? ZERO : debit + ? openingAmount.subtract(yearDebitAmount).add(yearCreditAmount) + : openingAmount.add(yearDebitAmount).subtract(yearCreditAmount); + BigDecimal yearOpeningQuantity = january ? ZERO : debit + ? openingQuantity.subtract(yearDebitQuantity).add(yearCreditQuantity) + : openingQuantity.add(yearDebitQuantity).subtract(yearCreditQuantity); + if (yearOpeningAmount.signum() < 0 || yearOpeningQuantity.signum() < 0) { + throw importRowException(source, "年初余额不能小于 0"); + } + target.setOpeningAmount(openingAmount).setOpeningQuantity(openingQuantity) + .setYearDebitAmount(yearDebitAmount).setYearDebitQuantity(yearDebitQuantity) + .setYearCreditAmount(yearCreditAmount).setYearCreditQuantity(yearCreditQuantity) + .setYearOpeningAmount(yearOpeningAmount).setYearOpeningQuantity(yearOpeningQuantity) + .setProfitLossAmount(january ? ZERO : NumberUtils.zeroIfNull(source.getProfitLossAmount())) + .setProfitLossQuantity(january ? ZERO : NumberUtils.zeroIfNull(source.getProfitLossQuantity())); + } + + private boolean isZeroRow(FmsInitialBalanceExcelVO row) { + List amounts = Arrays.asList(row.getOpeningQuantity(), row.getOpeningAmount(), + row.getYearDebitQuantity(), row.getYearDebitAmount(), row.getYearCreditQuantity(), + row.getYearCreditAmount(), row.getProfitLossQuantity(), row.getProfitLossAmount()); + return amounts.stream().map(NumberUtils::zeroIfNull).allMatch(amount -> amount.signum() == 0); + } + + private RuntimeException importRowException(FmsInitialBalanceExcelVO row, String message) { + return exception(INITIAL_BALANCE_IMPORT_ROW_INVALID, row.getRowNumber(), message); + } + + private void setZeroAmounts(FmsInitialBalanceRespVO respVO) { + respVO.setOpeningAmount(ZERO).setOpeningQuantity(ZERO).setYearDebitAmount(ZERO) + .setYearDebitQuantity(ZERO).setYearCreditAmount(ZERO).setYearCreditQuantity(ZERO) + .setYearOpeningAmount(ZERO).setYearOpeningQuantity(ZERO) + .setProfitLossAmount(ZERO).setProfitLossQuantity(ZERO); + } + + private void validateProfitLossBalance(FmsSubjectDO subject, BigDecimal yearOpeningAmount) { + if (ObjUtil.equal(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + && NumberUtils.zeroIfNull(yearOpeningAmount).signum() != 0) { + throw exception(INITIAL_BALANCE_PROFIT_LOSS_YEAR_OPENING_INVALID); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectService.java new file mode 100644 index 0000000..e4036f4 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectService.java @@ -0,0 +1,147 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectDeleteReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectStatusReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectUsageRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; + +import java.util.List; + +/** + * FMS 会计科目 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsSubjectService { + + /** + * 初始化账套会计科目 + * + * @param accountSetId 账套编号 + */ + void initializeDefaultSubjects(Long accountSetId); + + /** + * 创建会计科目 + * + * @param createReqVO 创建信息 + * @param userId 当前用户编号 + * @return 科目编号 + */ + Long createSubject(FmsSubjectSaveReqVO createReqVO, Long userId); + + /** + * 更新会计科目 + * + * @param updateReqVO 更新信息 + * @param userId 当前用户编号 + */ + void updateSubject(FmsSubjectSaveReqVO updateReqVO, Long userId); + + /** + * 删除会计科目 + * + * @param deleteReqVO 删除信息 + * @param userId 当前用户编号 + */ + void deleteSubjectList(FmsSubjectDeleteReqVO deleteReqVO, Long userId); + + /** + * 更新会计科目状态 + * + * @param statusReqVO 状态更新信息 + * @param userId 当前用户编号 + */ + void updateSubjectStatus(FmsSubjectStatusReqVO statusReqVO, Long userId); + + /** + * 获得会计科目 + * + * @param accountSetId 账套编号 + * @param id 科目编号 + * @param userId 当前用户编号 + * @return 会计科目 + */ + FmsSubjectDO getSubject(Long accountSetId, Long id, Long userId); + + /** + * 获得会计科目使用情况 + * + * @param accountSetId 账套编号 + * @param id 科目编号 + * @param userId 当前用户编号 + * @return 科目使用情况 + */ + FmsSubjectUsageRespVO getSubjectUsage(Long accountSetId, Long id, Long userId); + + /** + * 获得账套下的会计科目列表 + * + * @param accountSetId 账套编号 + * @param type 科目类型 + * @param userId 当前用户编号 + * @return 会计科目列表 + */ + List getSubjectList(Long accountSetId, Integer type, Long userId); + + /** + * 获得账套下的会计科目列表 + * + * @param listReqVO 列表查询条件 + * @param userId 当前用户编号 + * @return 会计科目列表 + */ + List getSubjectList(FmsSubjectListReqVO listReqVO, Long userId); + + /** + * 获得科目及其所有下级科目编号 + * + * @param accountSetId 账套编号 + * @param id 科目编号 + * @return 科目编号数组 + */ + List getSubjectIdListWithChildren(Long accountSetId, Long id); + + /** + * 获得使用辅助核算类别的会计科目数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @return 会计科目数量 + */ + Long getSubjectCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId); + + /** + * 获得使用币别的会计科目数量 + * + * @param accountSetId 账套编号 + * @param currencyId 币别编号 + * @return 会计科目数量 + */ + Long getSubjectCountByCurrencyId(Long accountSetId, Long currencyId); + + /** + * 按新的编码规则扩展会计科目编码 + * + * @param accountSetId 账套编号 + * @param oldRules 原编码规则 + * @param newRules 新编码规则 + */ + void expandSubjectCodes(Long accountSetId, List oldRules, List newRules); + + /** + * 导入会计科目 + * + * @param accountSetId 账套编号 + * @param importSubjects 导入数据 + * @param userId 当前用户编号 + * @return 导入结果 + */ + FmsSubjectImportRespVO importSubjectList(Long accountSetId, List importSubjects, + Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectServiceImpl.java new file mode 100644 index 0000000..ab2b68c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectServiceImpl.java @@ -0,0 +1,870 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectDeleteReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectStatusReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectUsageRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectTemplateDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsSubjectMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsSubjectTemplateMapper; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectCategoryEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingTemplateService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolationException; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CODE_RULE_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CURRENCY_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_HAS_CHILDREN; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_AUXILIARY_COMBINATION_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_AUXILIARY_MIGRATION_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CLOSING_SCHEME_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CLOSING_TEMPLATE_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_INITIAL_BALANCE_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_IMPORT_LIST_IS_EMPTY; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_NON_LEAF_CONFIG_IMMUTABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_PARENT_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_PARENT_CONFIG_INCOMPATIBLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_PARENT_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_QUANTITY_ACCOUNTING_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_USED_AUXILIARY_IMMUTABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_USED_BALANCE_DIRECTION_IMMUTABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_VOUCHER_ENTRY_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_VOUCHER_TEMPLATE_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_CREATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_CREATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_DELETE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_DELETE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_STATUS_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_STATUS_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_SUBJECT_UPDATE_SUCCESS; + +/** + * FMS 会计科目 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsSubjectServiceImpl implements FmsSubjectService { + + @Resource + private FmsSubjectMapper subjectMapper; + @Resource + private FmsSubjectTemplateMapper subjectTemplateMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsFinanceParameterService financeParameterService; + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + @Resource + private FmsAuxiliaryItemService auxiliaryItemService; + @Resource + private FmsCurrencyService currencyService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private FmsVoucherService voucherService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private FmsVoucherTemplateService voucherTemplateService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private FmsInitialBalanceService initialBalanceService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private FmsClosingSchemeService closingSchemeService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private FmsClosingTemplateService closingTemplateService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void initializeDefaultSubjects(Long accountSetId) { + // 1.1 查询标准科目模板并按编码排序 + List templates = subjectTemplateMapper.selectList(); + if (CollUtil.isEmpty(templates)) { + return; + } + templates.sort(Comparator.comparing(FmsSubjectTemplateDO::getCode)); + // 1.2 标准模板编码需要按账套科目编码规则转换后再创建 + String subjectCodeRule = financeParameterService.getFinanceParameter(accountSetId).getSubjectCodeRule(); + + // 2. 按模板层级创建账套科目 + Map templateSubjectIdMap = new HashMap<>(templates.size()); + for (FmsSubjectTemplateDO template : templates) { + FmsSubjectDO subject = BeanUtils.toBean(template, FmsSubjectDO.class) + .setId(null).setAccountSetId(accountSetId) + .setCode(financeParameterService.convertStandardSubjectCode(template.getCode(), subjectCodeRule)) + .setParentId(FmsSubjectTemplateDO.PARENT_ID_ROOT.equals(template.getParentId()) + ? FmsSubjectDO.PARENT_ID_ROOT : templateSubjectIdMap.get(template.getParentId())) + .setAuxiliaryTypeIds(Collections.emptyList()).setCurrencyIds(Collections.emptyList()); + subjectMapper.insert(subject); + templateSubjectIdMap.put(template.getId(), subject.getId()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_SUBJECT_TYPE, subType = FMS_SUBJECT_CREATE_SUB_TYPE, + bizNo = "{{#subjectId}}", success = FMS_SUBJECT_CREATE_SUCCESS) + public Long createSubject(FmsSubjectSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 查询财务参数 + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(createReqVO.getAccountSetId()); + // 1.3 校验科目编码唯一 + validateSubjectCodeUnique(null, createReqVO.getAccountSetId(), createReqVO.getCode()); + // 1.4 校验上级科目和科目编码规则 + FmsSubjectDO parent = validateParentSubject(createReqVO.getAccountSetId(), + createReqVO.getParentId(), createReqVO.getType(), createReqVO.getCategory()); + FmsSubjectUsageRespVO parentUsage = validateParentSubjectDataMigration(createReqVO, parent); + int level = parent == null ? 1 : parent.getLevel() + 1; + validateSubjectCodeRule(createReqVO.getCode(), level, parent, financeParameter); + // 1.5 校验辅助核算类别 + auxiliaryTypeService.validateAuxiliaryTypeList( + createReqVO.getAccountSetId(), createReqVO.getAuxiliaryTypeIds()); + // 1.6 校验外币核算币别 + validateCurrencyList(createReqVO.getAccountSetId(), createReqVO.getCurrencyIds()); + + // 2. 创建科目 + FmsSubjectDO subject = BeanUtils.toBean(createReqVO, FmsSubjectDO.class) + .setId(null).setLevel(level).setStatus(CommonStatusEnum.ENABLE.getStatus()); + subjectMapper.insert(subject); + + // 3. 迁移上级科目的历史业务数据 + if (parent != null && Boolean.TRUE.equals(parentUsage.getUsed())) { + voucherService.updateVoucherEntrySubject(createReqVO.getAccountSetId(), parent.getId(), subject); + initialBalanceService.updateInitialBalanceSubject( + createReqVO.getAccountSetId(), parent.getId(), subject.getId()); + auxiliaryCombinationService.updateAuxiliaryCombinationSubject( + createReqVO.getAccountSetId(), parent.getId(), subject.getId()); + } + // 父科目成为非末级后,辅助核算配置不再生效,避免残留配置继续占用辅助类别 + if (parent != null && CollUtil.isNotEmpty(parent.getAuxiliaryTypeIds())) { + subjectMapper.updateById(new FmsSubjectDO().setId(parent.getId()) + .setAuxiliaryTypeIds(Collections.emptyList())); + } + + // 4. 记录操作日志上下文 + LogRecordContext.putVariable("subjectId", subject.getId()); + return subject.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_SUBJECT_TYPE, subType = FMS_SUBJECT_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_SUBJECT_UPDATE_SUCCESS) + public void updateSubject(FmsSubjectSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 查询财务参数 + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter( + updateReqVO.getAccountSetId()); + // 1.3 校验科目存在 + FmsSubjectDO subject = validateSubjectExists(updateReqVO.getAccountSetId(), updateReqVO.getId()); + if (ObjUtil.notEqual(subject.getParentId(), updateReqVO.getParentId())) { + throw exception(SUBJECT_PARENT_INVALID); + } + // 1.4 校验已用科目的配置修改范围 + FmsSubjectUsageRespVO usage = buildSubjectUsage(updateReqVO.getAccountSetId(), subject.getId()); + validateSubjectConfigUpdate(subject, updateReqVO, usage); + // 1.5 校验科目编码唯一 + validateSubjectCodeUnique(subject.getId(), updateReqVO.getAccountSetId(), updateReqVO.getCode()); + // 1.6 校验上级科目和科目编码规则 + FmsSubjectDO parent = validateParentSubject(updateReqVO.getAccountSetId(), + updateReqVO.getParentId(), updateReqVO.getType(), updateReqVO.getCategory()); + validateSubjectCodeRule(updateReqVO.getCode(), subject.getLevel(), parent, financeParameter); + // 1.7 校验有下级科目时不能修改编码 + if (ObjUtil.notEqual(subject.getCode(), updateReqVO.getCode()) + && usage.getChildCount() > 0) { + throw exception(SUBJECT_HAS_CHILDREN); + } + // 1.8 校验辅助核算类别和外币核算币别 + List auxiliaryTypes = auxiliaryTypeService.validateAuxiliaryTypeList( + updateReqVO.getAccountSetId(), updateReqVO.getAuxiliaryTypeIds()); + validateCurrencyList(updateReqVO.getAccountSetId(), updateReqVO.getCurrencyIds()); + // 1.9 校验已用科目首次启用辅助核算的历史数据迁移项目 + Map migrationItemMap = validateSubjectAuxiliaryMigration( + subject, updateReqVO, usage); + + // 2. 更新科目 + subjectMapper.updateById(BeanUtils.toBean(updateReqVO, FmsSubjectDO.class) + .setParentId(subject.getParentId()).setLevel(subject.getLevel())); + + // 3. 迁移科目的历史辅助核算数据 + if (CollUtil.isNotEmpty(migrationItemMap)) { + Map auxiliaryTypeMap = convertMap(auxiliaryTypes, FmsAuxiliaryTypeDO::getId); + List combinationItems = convertList(updateReqVO.getAuxiliaryTypeIds(), typeId -> { + FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeMap.get(typeId); + FmsAuxiliaryItemDO auxiliaryItem = migrationItemMap.get(typeId); + return FmsAuxiliaryCombinationDO.AuxiliaryItem.builder() + .type(auxiliaryType.getType()).typeId(typeId) + .itemId(auxiliaryItem.getId()).name(auxiliaryItem.getName()).build(); + }); + FmsAuxiliaryCombinationDO combination = auxiliaryCombinationService.saveAuxiliaryCombination( + updateReqVO.getAccountSetId(), subject.getId(), combinationItems); + voucherService.migrateVoucherEntryAuxiliaries(updateReqVO.getAccountSetId(), subject.getId(), combination); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_SUBJECT_TYPE, subType = FMS_SUBJECT_DELETE_SUB_TYPE, + bizNo = "{{#subjects.iterator().next().id}}", success = FMS_SUBJECT_DELETE_SUCCESS) + public void deleteSubjectList(FmsSubjectDeleteReqVO deleteReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(deleteReqVO.getAccountSetId(), userId); + // 1.2 校验科目存在 + List subjects = subjectMapper.selectListByIdsAndAccountSetId( + deleteReqVO.getIds(), deleteReqVO.getAccountSetId()); + if (subjects.size() != deleteReqVO.getIds().size()) { + throw exception(SUBJECT_NOT_EXISTS); + } + // 1.3 校验科目没有下级 + if (subjectMapper.selectCountByParentIds(deleteReqVO.getIds()) > 0) { + throw exception(SUBJECT_HAS_CHILDREN); + } + // 1.4 校验科目未被业务引用 + validateSubjectNotInUse(deleteReqVO.getAccountSetId(), deleteReqVO.getIds()); + + // 2. 删除科目 + subjectMapper.deleteByIds(deleteReqVO.getIds()); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("subjects", subjects); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_SUBJECT_TYPE, subType = FMS_SUBJECT_STATUS_SUB_TYPE, + bizNo = "{{#statusReqVO.accountSetId}}", success = FMS_SUBJECT_STATUS_SUCCESS) + public void updateSubjectStatus(FmsSubjectStatusReqVO statusReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(statusReqVO.getAccountSetId(), userId); + // 1.2 校验科目存在 + List subjects = subjectMapper.selectListByIdsAndAccountSetId( + statusReqVO.getIds(), statusReqVO.getAccountSetId()); + if (subjects.size() != statusReqVO.getIds().size()) { + throw exception(SUBJECT_NOT_EXISTS); + } + + // 2. 递归收集选中科目的全部子孙,批量更新状态 + Set subjectIds = getSubjectIdSetWithChildren( + statusReqVO.getAccountSetId(), statusReqVO.getIds()); + subjectMapper.updateStatusByIdsAndAccountSetId( + subjectIds, statusReqVO.getAccountSetId(), + new FmsSubjectDO().setStatus(statusReqVO.getStatus())); + } + + @Override + public FmsSubjectDO getSubject(Long accountSetId, Long id, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询科目 + return subjectMapper.selectByIdAndAccountSetId(id, accountSetId); + } + + @Override + public FmsSubjectUsageRespVO getSubjectUsage(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 校验科目存在 + validateSubjectExists(accountSetId, id); + + // 2. 统计科目使用情况 + return buildSubjectUsage(accountSetId, id); + } + + @Override + public List getSubjectList(Long accountSetId, Integer type, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询科目列表 + return subjectMapper.selectListByAccountSetIdAndType(accountSetId, type); + } + + @Override + public List getSubjectList(FmsSubjectListReqVO listReqVO, Long userId) { + return getSubjectList(listReqVO.getAccountSetId(), listReqVO.getType(), userId); + } + + @Override + public List getSubjectIdListWithChildren(Long accountSetId, Long id) { + // 1. 校验科目存在 + validateSubjectExists(accountSetId, id); + + // 2. 获得科目及全部下级科目 + return new ArrayList<>(getSubjectIdSetWithChildren(accountSetId, Collections.singletonList(id))); + } + + /** + * 按层查询并收集指定科目及全部子孙科目编号 + * + * @param accountSetId 账套编号 + * @param ids 起始科目编号集合 + * @return 科目编号集合 + */ + private Set getSubjectIdSetWithChildren(Long accountSetId, Collection ids) { + Set subjectIds = new LinkedHashSet<>(ids); + Collection parentIds = ids; + for (int i = 0; i < Short.MAX_VALUE; i++) { + List children = subjectMapper.selectListByParentIds(accountSetId, parentIds); + if (CollUtil.isEmpty(children)) { + break; + } + parentIds = convertList(children, FmsSubjectDO::getId); + subjectIds.addAll(parentIds); + } + return subjectIds; + } + + @Override + public Long getSubjectCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId) { + List subjects = subjectMapper.selectListByAccountSetIdAndType(accountSetId, null); + return (long) filterList(subjects, + subject -> CollUtil.contains(subject.getAuxiliaryTypeIds(), auxiliaryTypeId)).size(); + } + + @Override + public Long getSubjectCountByCurrencyId(Long accountSetId, Long currencyId) { + List subjects = subjectMapper.selectListByAccountSetIdAndType(accountSetId, null); + return (long) filterList(subjects, + subject -> CollUtil.contains(subject.getCurrencyIds(), currencyId)).size(); + } + + @Override + public void expandSubjectCodes(Long accountSetId, List oldRules, List newRules) { + // 1. 查询账套全部会计科目 + List subjects = subjectMapper.selectListByAccountSetIdAndType(accountSetId, null); + if (CollUtil.isEmpty(subjects)) { + return; + } + + // 2. 按各级编码增量扩展会计科目编码 + List updateSubjects = new ArrayList<>(); + for (FmsSubjectDO subject : subjects) { + String expandedCode = expandSubjectCode(subject.getCode(), subject.getLevel(), oldRules, newRules); + if (ObjUtil.notEqual(expandedCode, subject.getCode())) { + updateSubjects.add(new FmsSubjectDO().setId(subject.getId()).setCode(expandedCode)); + } + } + if (CollUtil.isNotEmpty(updateSubjects)) { + subjectMapper.updateBatch(updateSubjects); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FmsSubjectImportRespVO importSubjectList(Long accountSetId, + List importSubjects, Long userId) { + // 1.1 校验导入数据非空 + if (CollUtil.isEmpty(importSubjects)) { + throw exception(SUBJECT_IMPORT_LIST_IS_EMPTY); + } + // 1.2 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.3 加载导入依赖 + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(accountSetId); + Map subjectMap = convertMap( + subjectMapper.selectListByAccountSetIdAndType(accountSetId, null), FmsSubjectDO::getCode); + Map auxiliaryTypeIdMap = auxiliaryTypeService.getAuxiliaryTypeIdMap(accountSetId); + + // 2. 按科目级次依次导入 + List sortedSubjects = new ArrayList<>(importSubjects); + sortedSubjects.sort(Comparator.comparingInt(item -> StrUtil.length(item.getCode()))); + Set importCodes = new HashSet<>(); + FmsSubjectImportRespVO respVO = new FmsSubjectImportRespVO() + .setTotalCount(importSubjects.size()).setSuccessSubjectCodes(new ArrayList<>()) + .setFailureReasons(new LinkedHashMap<>()); + for (int index = 0; index < sortedSubjects.size(); index++) { + FmsSubjectImportExcelVO importSubject = sortedSubjects.get(index); + String subjectCode = importSubject.getCode(); + String label = buildImportLabel(index + 2, subjectCode, importSubject.getName()); + try { + // 2.1 校验导入字段和科目编码 + ValidationUtils.validate(importSubject); + if (!importCodes.add(subjectCode)) { + throw new IllegalArgumentException("导入文件内科目编码重复"); + } + FmsSubjectDO existingSubject = subjectMap.get(subjectCode); + FmsSubjectCategoryEnum category = FmsSubjectCategoryEnum.valueOfName( + importSubject.getCategoryName()); + if (category == null) { + throw new IllegalArgumentException("科目类别不存在"); + } + FmsDebitCreditDirectionEnum direction = FmsDebitCreditDirectionEnum.valueOfName( + importSubject.getBalanceDirection()); + if (direction == null) { + throw new IllegalArgumentException("余额方向只能填写借或贷"); + } + + // 2.2 解析上级科目和辅助核算类别 + FmsSubjectDO parent = resolveImportParent(importSubject.getParentSubjectCode(), subjectMap); + if (parent != null && (ObjUtil.notEqual(parent.getType(), category.getType()) + || ObjUtil.notEqual(parent.getCategory(), category.getCategory()))) { + throw new IllegalArgumentException("上级科目与当前科目类型或类别不一致"); + } + List auxiliaryTypeIds = resolveImportAuxiliaryTypeIds( + importSubject.getAuxiliaryNames(), auxiliaryTypeIdMap); + + // 2.3 构造并创建或安全覆盖科目 + FmsSubjectSaveReqVO reqVO = new FmsSubjectSaveReqVO(); + reqVO.setAccountSetId(accountSetId).setCode(subjectCode) + .setName(importSubject.getName()) + .setParentId(parent == null ? FmsSubjectDO.PARENT_ID_ROOT : parent.getId()) + .setType(category.getType()).setCategory(category.getCategory()) + .setBalanceDirection(direction.getType()).setAuxiliaryTypeIds(auxiliaryTypeIds) + .setCurrencyIds(Collections.emptyList()).setQuantityAccounting(false).setCash(false); + FmsSubjectDO subject; + if (existingSubject == null) { + subject = createImportedSubject(reqVO, parent, financeParameter); + } else { + subject = updateImportedSubject(existingSubject, reqVO, parent, financeParameter); + } + + // 2.4 记录导入结果 + subjectMap.put(subjectCode, subject); + respVO.getSuccessSubjectCodes().add(subjectCode); + } catch (ConstraintViolationException exception) { + respVO.getFailureReasons().put(label, exception.getMessage()); + } catch (RuntimeException exception) { + respVO.getFailureReasons().put(label, StrUtil.blankToDefault(exception.getMessage(), "导入失败")); + } + } + return respVO; + } + + private FmsSubjectDO createImportedSubject(FmsSubjectSaveReqVO reqVO, FmsSubjectDO parent, + FmsFinanceParameterDO financeParameter) { + // 1.1 校验科目编码唯一 + validateSubjectCodeUnique(null, reqVO.getAccountSetId(), reqVO.getCode()); + // 1.2 校验科目层级和编码规则 + int level = parent == null ? 1 : parent.getLevel() + 1; + validateSubjectCodeRule(reqVO.getCode(), level, parent, financeParameter); + // 1.3 校验上级科目没有历史业务数据 + validateParentSubjectUnused(reqVO.getAccountSetId(), parent); + // 1.4 校验辅助核算类别 + auxiliaryTypeService.validateAuxiliaryTypeList(reqVO.getAuxiliaryTypeIds()); + + // 2. 创建科目 + FmsSubjectDO subject = BeanUtils.toBean(reqVO, FmsSubjectDO.class) + .setId(null).setLevel(level).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setCurrencyIds(Collections.emptyList()); + subjectMapper.insert(subject); + return subject; + } + + /** + * 覆盖导入时已存在但未被业务使用的科目 + * + * @param subject 已存在的科目 + * @param reqVO 导入科目信息 + * @param parent 新上级科目 + * @param financeParameter 财务参数 + * @return 覆盖后的科目 + */ + private FmsSubjectDO updateImportedSubject(FmsSubjectDO subject, FmsSubjectSaveReqVO reqVO, + FmsSubjectDO parent, FmsFinanceParameterDO financeParameter) { + // 1. 仅允许覆盖没有下级、没有任何业务引用的科目 + if (subjectMapper.selectCountByParentId(subject.getId()) > 0) { + throw new IllegalArgumentException("已有下级科目,不能覆盖"); + } + validateSubjectNotInUse(reqVO.getAccountSetId(), Collections.singletonList(subject.getId())); + + // 2. 校验新上级和导入配置 + int level = parent == null ? 1 : parent.getLevel() + 1; + validateSubjectCodeRule(reqVO.getCode(), level, parent, financeParameter); + validateParentSubjectUnused(reqVO.getAccountSetId(), parent); + auxiliaryTypeService.validateAuxiliaryTypeList(reqVO.getAuxiliaryTypeIds()); + + // 3. 保留主键和状态,覆盖可导入的科目配置 + FmsSubjectDO updateObj = BeanUtils.toBean(reqVO, FmsSubjectDO.class) + .setId(subject.getId()).setLevel(level).setStatus(subject.getStatus()) + .setCurrencyIds(Collections.emptyList()); + subjectMapper.updateById(updateObj); + if (parent != null && CollUtil.isNotEmpty(parent.getAuxiliaryTypeIds())) { + subjectMapper.updateById(new FmsSubjectDO().setId(parent.getId()).setAuxiliaryTypeIds(Collections.emptyList())); + } + return updateObj; + } + + /** + * 按新旧编码规则扩展科目编码 + * + * @param subjectCode 科目编码 + * @param level 科目层级 + * @param oldRules 原编码规则 + * @param newRules 新编码规则 + * @return 扩展后的科目编码 + */ + private String expandSubjectCode(String subjectCode, Integer level, + List oldRules, List newRules) { + StringBuilder result = new StringBuilder(); + int offset = 0; + for (int index = 0; index < level; index++) { + int oldLength = oldRules.get(index); + int increment = newRules.get(index) - oldLength; + String segment = subjectCode.substring(offset, offset + oldLength); + if (increment > 0) { + if (index == 0) { + result.append(segment.charAt(0)).append(CharSequenceUtil.repeat('0', increment)) + .append(segment.substring(1)); + } else { + result.append(CharSequenceUtil.repeat('0', increment)).append(segment); + } + } else { + result.append(segment); + } + offset += oldLength; + } + return result.toString(); + } + + private FmsSubjectDO resolveImportParent(String parentSubjectCode, Map subjectMap) { + if (StrUtil.isBlank(parentSubjectCode) || "0".equals(parentSubjectCode)) { + return null; + } + FmsSubjectDO parent = subjectMap.get(parentSubjectCode); + if (parent == null) { + throw new IllegalArgumentException("上级科目编码不存在"); + } + return parent; + } + + private List resolveImportAuxiliaryTypeIds(String auxiliaryNames, Map auxiliaryTypeIdMap) { + if (StrUtil.isBlank(auxiliaryNames)) { + return Collections.emptyList(); + } + List auxiliaryTypeIds = new ArrayList<>(); + for (String auxiliaryName : StrUtil.split(auxiliaryNames.replace('、', '/'), '/')) { + Long auxiliaryTypeId = auxiliaryTypeIdMap.get(auxiliaryName); + if (auxiliaryTypeId == null) { + throw new IllegalArgumentException("辅助核算类别“" + auxiliaryName + "”不存在"); + } + auxiliaryTypeIds.add(auxiliaryTypeId); + } + return auxiliaryTypeIds; + } + + private String buildImportLabel(int rowNumber, String subjectCode, String name) { + return "第 " + rowNumber + " 行(" + StrUtil.blankToDefault(subjectCode, "未填写编码") + + (StrUtil.isBlank(name) ? "" : " " + name) + ")"; + } + + private FmsSubjectDO validateSubjectExists(Long accountSetId, Long id) { + FmsSubjectDO subject = subjectMapper.selectById(id); + if (subject == null || ObjUtil.notEqual(subject.getAccountSetId(), accountSetId)) { + throw exception(SUBJECT_NOT_EXISTS); + } + return subject; + } + + private void validateSubjectNotInUse(Long accountSetId, List subjectIds) { + // 1. 校验科目未被凭证分录使用 + Long voucherEntryCount = voucherService.getVoucherEntryCountBySubjectIds(accountSetId, subjectIds); + if (voucherEntryCount > 0) { + throw exception(SUBJECT_VOUCHER_ENTRY_IN_USE, voucherEntryCount); + } + // 2. 校验科目未被凭证模板使用 + Long voucherTemplateCount = voucherTemplateService.getVoucherTemplateCountBySubjectIds( + accountSetId, subjectIds); + if (voucherTemplateCount > 0) { + throw exception(SUBJECT_VOUCHER_TEMPLATE_IN_USE, voucherTemplateCount); + } + // 3. 校验科目未被初始余额使用 + Long initialBalanceCount = initialBalanceService.getInitialBalanceCountBySubjectIds( + accountSetId, subjectIds); + if (initialBalanceCount > 0) { + throw exception(SUBJECT_INITIAL_BALANCE_IN_USE, initialBalanceCount); + } + // 4. 校验科目未被辅助核算组合使用 + Long auxiliaryCombinationCount = auxiliaryCombinationService.getAuxiliaryCombinationCountBySubjectIds( + accountSetId, subjectIds); + if (auxiliaryCombinationCount > 0) { + throw exception(SUBJECT_AUXILIARY_COMBINATION_IN_USE, auxiliaryCombinationCount); + } + // 5. 校验科目未被结账方案使用 + Long closingSchemeCount = closingSchemeService.getClosingSchemeCountBySubjectIds(accountSetId, subjectIds); + if (closingSchemeCount > 0) { + throw exception(SUBJECT_CLOSING_SCHEME_IN_USE, closingSchemeCount); + } + // 6. 校验科目未被结账模板使用 + Long closingTemplateCount = closingTemplateService.getClosingTemplateCountBySubjectIds( + accountSetId, subjectIds); + if (closingTemplateCount > 0) { + throw exception(SUBJECT_CLOSING_TEMPLATE_IN_USE, closingTemplateCount); + } + } + + /** + * 构建科目使用情况 + * + * @param accountSetId 账套编号 + * @param subjectId 科目编号 + * @return 科目使用情况 + */ + private FmsSubjectUsageRespVO buildSubjectUsage(Long accountSetId, Long subjectId) { + // 1. 查询科目的业务引用数量 + List subjectIds = Collections.singletonList(subjectId); + Long voucherEntryCount = ObjUtil.defaultIfNull( + voucherService.getVoucherEntryCountBySubjectIds(accountSetId, subjectIds), 0L); + Long initialBalanceCount = ObjUtil.defaultIfNull( + initialBalanceService.getInitialBalanceCountBySubjectIds(accountSetId, subjectIds), 0L); + Long auxiliaryCombinationCount = ObjUtil.defaultIfNull( + auxiliaryCombinationService.getAuxiliaryCombinationCountBySubjectIds(accountSetId, subjectIds), 0L); + + // 2. 查询科目的数量核算数据数量 + Long voucherEntryQuantityCount = ObjUtil.defaultIfNull( + voucherService.getVoucherEntryQuantityCountBySubjectIds(accountSetId, subjectIds), 0L); + Long initialBalanceQuantityCount = ObjUtil.defaultIfNull( + initialBalanceService.getInitialBalanceQuantityCountBySubjectIds(accountSetId, subjectIds), 0L); + Long quantityDataCount = voucherEntryQuantityCount + initialBalanceQuantityCount; + + // 3. 构建科目使用情况 + return new FmsSubjectUsageRespVO() + .setChildCount(subjectMapper.selectCountByParentId(subjectId)) + .setVoucherEntryCount(voucherEntryCount).setInitialBalanceCount(initialBalanceCount) + .setAuxiliaryCombinationCount(auxiliaryCombinationCount).setQuantityDataCount(quantityDataCount) + .setUsed(voucherEntryCount + initialBalanceCount + auxiliaryCombinationCount > 0); + } + + /** + * 校验上级科目未被业务使用 + * + * @param accountSetId 账套编号 + * @param parent 上级科目 + */ + private void validateParentSubjectUnused(Long accountSetId, FmsSubjectDO parent) { + if (parent != null && Boolean.TRUE.equals(buildSubjectUsage(accountSetId, parent.getId()).getUsed())) { + throw exception(SUBJECT_PARENT_IN_USE); + } + } + + /** + * 校验上级科目历史数据迁移 + * + * @param createReqVO 创建信息 + * @param parent 上级科目 + * @return 上级科目使用情况 + */ + private FmsSubjectUsageRespVO validateParentSubjectDataMigration( + FmsSubjectSaveReqVO createReqVO, FmsSubjectDO parent) { + if (parent == null) { + return new FmsSubjectUsageRespVO().setUsed(false); + } + FmsSubjectUsageRespVO usage = buildSubjectUsage(createReqVO.getAccountSetId(), parent.getId()); + if (Boolean.FALSE.equals(usage.getUsed())) { + return usage; + } + // 只有新增第一个下级科目并明确确认时,才允许迁移上级科目的历史数据 + if (usage.getChildCount() > 0 || !Boolean.TRUE.equals(createReqVO.getMigrateParentData())) { + throw exception(SUBJECT_PARENT_IN_USE); + } + // 历史数据使用原科目核算配置,新子科目必须完整继承相关配置 + boolean auxiliarySame = new HashSet<>(CollUtil.emptyIfNull(parent.getAuxiliaryTypeIds())) + .equals(new HashSet<>(CollUtil.emptyIfNull(createReqVO.getAuxiliaryTypeIds()))); + boolean currencySame = new HashSet<>(CollUtil.emptyIfNull(parent.getCurrencyIds())) + .equals(new HashSet<>(CollUtil.emptyIfNull(createReqVO.getCurrencyIds()))); + boolean quantityUnitSame = Boolean.FALSE.equals(parent.getQuantityAccounting()) + || ObjUtil.equal(parent.getQuantityUnit(), createReqVO.getQuantityUnit()); + if (ObjUtil.notEqual(parent.getBalanceDirection(), createReqVO.getBalanceDirection()) + || !auxiliarySame || !currencySame + || ObjUtil.notEqual(parent.getQuantityAccounting(), createReqVO.getQuantityAccounting()) + || !quantityUnitSame) { + throw exception(SUBJECT_PARENT_CONFIG_INCOMPATIBLE); + } + return usage; + } + + /** + * 校验已用科目的配置修改范围 + * + * @param subject 科目 + * @param updateReqVO 更新信息 + * @param usage 科目使用情况 + */ + private void validateSubjectConfigUpdate(FmsSubjectDO subject, FmsSubjectSaveReqVO updateReqVO, + FmsSubjectUsageRespVO usage) { + boolean auxiliaryChanged = !new HashSet<>(CollUtil.emptyIfNull(subject.getAuxiliaryTypeIds())) + .equals(new HashSet<>(CollUtil.emptyIfNull(updateReqVO.getAuxiliaryTypeIds()))); + // 非末级科目不允许修改类型、类别和辅助核算 + if (usage.getChildCount() > 0 && (ObjUtil.notEqual(subject.getType(), updateReqVO.getType()) + || ObjUtil.notEqual(subject.getCategory(), updateReqVO.getCategory()) || auxiliaryChanged)) { + throw exception(SUBJECT_NON_LEAF_CONFIG_IMMUTABLE); + } + // 已用科目不允许修改余额方向 + if (Boolean.TRUE.equals(usage.getUsed()) + && ObjUtil.notEqual(subject.getBalanceDirection(), updateReqVO.getBalanceDirection())) { + throw exception(SUBJECT_USED_BALANCE_DIRECTION_IMMUTABLE); + } + // 已用科目仅允许从未启用辅助核算迁移为启用状态,其他辅助核算变更仍禁止 + boolean auxiliaryMigration = CollUtil.isEmpty(subject.getAuxiliaryTypeIds()) + && CollUtil.isNotEmpty(updateReqVO.getAuxiliaryTypeIds()); + if (Boolean.TRUE.equals(usage.getUsed()) && auxiliaryChanged && !auxiliaryMigration) { + throw exception(SUBJECT_USED_AUXILIARY_IMMUTABLE); + } + // 存在实际数量时不允许关闭数量核算 + if (Boolean.TRUE.equals(subject.getQuantityAccounting()) + && Boolean.FALSE.equals(updateReqVO.getQuantityAccounting()) && usage.getQuantityDataCount() > 0) { + throw exception(SUBJECT_QUANTITY_ACCOUNTING_IN_USE); + } + } + + /** + * 校验已用科目首次启用辅助核算的历史数据迁移项目 + * + * @param subject 科目 + * @param updateReqVO 更新信息 + * @param usage 科目使用情况 + * @return 迁移项目 Map,键为辅助核算类别编号 + */ + private Map validateSubjectAuxiliaryMigration(FmsSubjectDO subject, + FmsSubjectSaveReqVO updateReqVO, FmsSubjectUsageRespVO usage) { + // 1. 未触发历史数据迁移时,无需校验迁移项目 + if (usage.getVoucherEntryCount() <= 0 || CollUtil.isNotEmpty(subject.getAuxiliaryTypeIds()) + || CollUtil.isEmpty(updateReqVO.getAuxiliaryTypeIds())) { + return Collections.emptyMap(); + } + + // 2. 校验每个新增辅助核算类别只配置一个迁移项目 + List mappings = updateReqVO.getAuxiliaryMappings(); + if (CollUtil.size(mappings) != updateReqVO.getAuxiliaryTypeIds().size()) { + throw exception(SUBJECT_AUXILIARY_MIGRATION_INVALID); + } + Map itemIdMap = new LinkedHashMap<>(); + for (FmsSubjectSaveReqVO.AuxiliaryMapping mapping : mappings) { + if (mapping == null || mapping.getTypeId() == null || mapping.getItemId() == null + || itemIdMap.put(mapping.getTypeId(), mapping.getItemId()) != null) { + throw exception(SUBJECT_AUXILIARY_MIGRATION_INVALID); + } + } + if (!new HashSet<>(updateReqVO.getAuxiliaryTypeIds()).equals(itemIdMap.keySet())) { + throw exception(SUBJECT_AUXILIARY_MIGRATION_INVALID); + } + + // 3. 校验迁移项目属于对应类别且处于启用状态 + List items = auxiliaryItemService.validateAuxiliaryItemList( + updateReqVO.getAccountSetId(), itemIdMap.values()); + Map itemMap = convertMap(items, FmsAuxiliaryItemDO::getAuxiliaryTypeId); + if (itemMap.size() != itemIdMap.size()) { + throw exception(SUBJECT_AUXILIARY_MIGRATION_INVALID); + } + for (Map.Entry entry : itemIdMap.entrySet()) { + FmsAuxiliaryItemDO item = itemMap.get(entry.getKey()); + if (item == null || ObjUtil.notEqual(item.getId(), entry.getValue()) + || ObjUtil.notEqual(item.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(SUBJECT_AUXILIARY_MIGRATION_INVALID); + } + } + return itemMap; + } + + private void validateSubjectCodeUnique(Long id, Long accountSetId, String subjectCode) { + FmsSubjectDO subject = subjectMapper.selectByAccountSetIdAndCode(accountSetId, subjectCode); + if (subject != null && ObjUtil.notEqual(subject.getId(), id)) { + throw exception(SUBJECT_CODE_DUPLICATE); + } + } + + private FmsSubjectDO validateParentSubject(Long accountSetId, Long parentId, Integer type, Integer category) { + if (FmsSubjectDO.PARENT_ID_ROOT.equals(parentId)) { + return null; + } + FmsSubjectDO parent = subjectMapper.selectById(parentId); + if (parent == null || ObjUtil.notEqual(parent.getAccountSetId(), accountSetId) + || ObjUtil.notEqual(parent.getType(), type) + || ObjUtil.notEqual(parent.getCategory(), category)) { + throw exception(SUBJECT_PARENT_INVALID); + } + return parent; + } + + /** + * 校验科目编码是否符合当前科目级次规则 + * + * @param subjectCode 科目编码 + * @param level 科目层级 + * @param parent 上级科目 + * @param financeParameter 财务参数 + */ + private void validateSubjectCodeRule(String subjectCode, int level, FmsSubjectDO parent, + FmsFinanceParameterDO financeParameter) { + String[] levels = financeParameter.getSubjectCodeRule().split("-"); + if (level > levels.length || !subjectCode.matches("\\d+")) { + throw exception(SUBJECT_CODE_RULE_INVALID); + } + int expectedLength = 0; + for (int index = 0; index < level; index++) { + expectedLength += Integer.parseInt(levels[index]); + } + if (subjectCode.length() != expectedLength + || parent != null && !subjectCode.startsWith(parent.getCode())) { + throw exception(SUBJECT_CODE_RULE_INVALID); + } + } + + /** + * 校验外币核算币别,不允许选择本位币 + * + * @param accountSetId 账套编号 + * @param currencyIds 币别编号数组 + */ + private void validateCurrencyList(Long accountSetId, List currencyIds) { + List currencies = currencyService.validateCurrencyList(accountSetId, currencyIds); + if (CollUtil.findOne(currencies, currency -> Boolean.TRUE.equals(currency.getStandard())) != null) { + throw exception(SUBJECT_CURRENCY_INVALID); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateService.java new file mode 100644 index 0000000..008515c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateService.java @@ -0,0 +1,127 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory.FmsVoucherTemplateCategorySaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * FMS 凭证模板 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsVoucherTemplateService { + + /** + * 创建凭证模板分类 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 分类编号 + */ + Long createTemplateCategory(FmsVoucherTemplateCategorySaveReqVO createReqVO, Long userId); + + /** + * 更新凭证模板分类 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateTemplateCategory(FmsVoucherTemplateCategorySaveReqVO updateReqVO, Long userId); + + /** + * 删除凭证模板分类 + * + * @param accountSetId 账套编号 + * @param id 分类编号 + * @param userId 用户编号 + */ + void deleteTemplateCategory(Long accountSetId, Long id, Long userId); + + /** + * 获得凭证模板分类列表 + * + * @param accountSetId 账套编号 + * @return 凭证模板分类列表 + */ + List getTemplateCategoryList(Long accountSetId); + + /** + * 获得凭证模板分类 Map + * + * @param accountSetId 账套编号 + * @return 凭证模板分类 Map + */ + default Map getTemplateCategoryMap(Long accountSetId) { + return convertMap(getTemplateCategoryList(accountSetId), FmsVoucherTemplateCategoryDO::getId); + } + + /** + * 创建凭证模板 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 模板编号 + */ + Long createVoucherTemplate(FmsVoucherTemplateSaveReqVO createReqVO, Long userId); + + /** + * 更新凭证模板 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateVoucherTemplate(FmsVoucherTemplateSaveReqVO updateReqVO, Long userId); + + /** + * 删除凭证模板 + * + * @param accountSetId 账套编号 + * @param id 模板编号 + * @param userId 用户编号 + */ + void deleteVoucherTemplate(Long accountSetId, Long id, Long userId); + + /** + * 获得凭证模板列表 + * + * @param accountSetId 账套编号 + * @return 凭证模板列表 + */ + List getVoucherTemplateList(Long accountSetId); + + /** + * 获得使用指定科目的凭证模板数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号数组 + * @return 凭证模板数量 + */ + Long getVoucherTemplateCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 获得使用指定辅助核算项目的凭证模板数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryItemIds 辅助核算项目编号数组 + * @return 凭证模板数量 + */ + Long getVoucherTemplateCountByAuxiliaryItemIds( + Long accountSetId, Collection auxiliaryItemIds); + + /** + * 获得使用指定辅助核算类别的凭证模板数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @return 凭证模板数量 + */ + Long getVoucherTemplateCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateServiceImpl.java new file mode 100644 index 0000000..09e9f13 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateServiceImpl.java @@ -0,0 +1,370 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory.FmsVoucherTemplateCategorySaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateEntryVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsVoucherTemplateCategoryMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsVoucherTemplateMapper; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; + +/** + * FMS 凭证模板 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsVoucherTemplateServiceImpl implements FmsVoucherTemplateService { + + @Resource + private FmsVoucherTemplateCategoryMapper templateCategoryMapper; + + @Resource + private FmsVoucherTemplateMapper templateMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsAuxiliaryItemService auxiliaryItemService; + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + + @Override + @LogRecord(type = FMS_VOUCHER_TEMPLATE_CATEGORY_TYPE, + subType = FMS_VOUCHER_TEMPLATE_CATEGORY_CREATE_SUB_TYPE, + bizNo = "{{#categoryId}}", success = FMS_VOUCHER_TEMPLATE_CATEGORY_CREATE_SUCCESS) + public Long createTemplateCategory(FmsVoucherTemplateCategorySaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验分类名称 + validateTemplateCategoryNameUnique(null, createReqVO.getAccountSetId(), createReqVO.getName()); + + // 2. 创建模板分类 + FmsVoucherTemplateCategoryDO category = BeanUtils.toBean( + createReqVO, FmsVoucherTemplateCategoryDO.class).setId(null); + templateCategoryMapper.insert(category); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("categoryId", category.getId()); + return category.getId(); + } + + @Override + @LogRecord(type = FMS_VOUCHER_TEMPLATE_CATEGORY_TYPE, + subType = FMS_VOUCHER_TEMPLATE_CATEGORY_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_VOUCHER_TEMPLATE_CATEGORY_UPDATE_SUCCESS) + public void updateTemplateCategory(FmsVoucherTemplateCategorySaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验模板分类 + validateTemplateCategory(updateReqVO.getAccountSetId(), updateReqVO.getId()); + // 1.3 校验分类名称唯一 + validateTemplateCategoryNameUnique(updateReqVO.getId(), updateReqVO.getAccountSetId(), updateReqVO.getName()); + + // 2. 更新模板分类 + templateCategoryMapper.updateById(new FmsVoucherTemplateCategoryDO() + .setId(updateReqVO.getId()).setName(updateReqVO.getName())); + } + + @Override + @LogRecord(type = FMS_VOUCHER_TEMPLATE_CATEGORY_TYPE, + subType = FMS_VOUCHER_TEMPLATE_CATEGORY_DELETE_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_VOUCHER_TEMPLATE_CATEGORY_DELETE_SUCCESS) + public void deleteTemplateCategory(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验模板分类 + FmsVoucherTemplateCategoryDO category = validateTemplateCategory(accountSetId, id); + // 1.3 校验模板分类未被使用 + if (templateMapper.selectCountByCategoryId(id) > 0) { + throw exception(VOUCHER_TEMPLATE_CATEGORY_IN_USE); + } + + // 2. 删除模板分类 + templateCategoryMapper.deleteById(id); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("category", category); + } + + @Override + public List getTemplateCategoryList(Long accountSetId) { + return templateCategoryMapper.selectListByAccountSetId(accountSetId); + } + + @Override + @LogRecord(type = FMS_VOUCHER_TEMPLATE_TYPE, subType = FMS_VOUCHER_TEMPLATE_CREATE_SUB_TYPE, + bizNo = "{{#templateId}}", success = FMS_VOUCHER_TEMPLATE_CREATE_SUCCESS) + public Long createVoucherTemplate(FmsVoucherTemplateSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验模板分类 + validateTemplateCategory(createReqVO.getAccountSetId(), createReqVO.getCategoryId()); + // 1.3 校验并构造模板分录 + List entries = validateAndBuildTemplateEntries( + createReqVO.getAccountSetId(), createReqVO.getEntries(), userId); + + // 2. 创建凭证模板 + FmsVoucherTemplateDO template = BeanUtils.toBean(createReqVO, FmsVoucherTemplateDO.class) + .setId(null).setEntries(entries); + templateMapper.insert(template); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("templateId", template.getId()); + return template.getId(); + } + + @Override + @LogRecord(type = FMS_VOUCHER_TEMPLATE_TYPE, subType = FMS_VOUCHER_TEMPLATE_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_VOUCHER_TEMPLATE_UPDATE_SUCCESS) + public void updateVoucherTemplate(FmsVoucherTemplateSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验凭证模板 + validateVoucherTemplate(updateReqVO.getAccountSetId(), updateReqVO.getId()); + // 1.3 校验模板分类 + validateTemplateCategory(updateReqVO.getAccountSetId(), updateReqVO.getCategoryId()); + // 1.4 校验并构造模板分录 + List entries = validateAndBuildTemplateEntries( + updateReqVO.getAccountSetId(), updateReqVO.getEntries(), userId); + + // 2. 更新凭证模板 + templateMapper.updateById(new FmsVoucherTemplateDO().setId(updateReqVO.getId()) + .setName(updateReqVO.getName()) + .setCategoryId(updateReqVO.getCategoryId()).setEntries(entries)); + } + + @Override + @LogRecord(type = FMS_VOUCHER_TEMPLATE_TYPE, subType = FMS_VOUCHER_TEMPLATE_DELETE_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_VOUCHER_TEMPLATE_DELETE_SUCCESS) + public void deleteVoucherTemplate(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验凭证模板 + FmsVoucherTemplateDO template = validateVoucherTemplate(accountSetId, id); + + // 2. 删除凭证模板 + templateMapper.deleteById(id); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("template", template); + } + + @Override + public List getVoucherTemplateList(Long accountSetId) { + return templateMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public Long getVoucherTemplateCountBySubjectIds(Long accountSetId, Collection subjectIds) { + // 1. 处理空参数 + if (CollUtil.isEmpty(subjectIds)) { + return 0L; + } + + // 2. 查询凭证模板列表 + List templates = templateMapper.selectListByAccountSetId(accountSetId); + + // 3. 计算使用指定科目的凭证模板数量 + Set subjectIdSet = new HashSet<>(subjectIds); + return (long) filterList(templates, template -> CollUtil.isNotEmpty(template.getEntries()) + && template.getEntries().stream() + .map(FmsVoucherTemplateDO.Entry::getSubjectId) + .anyMatch(subjectIdSet::contains)).size(); + } + + @Override + public Long getVoucherTemplateCountByAuxiliaryItemIds( + Long accountSetId, Collection auxiliaryItemIds) { + // 1. 处理空参数 + if (CollUtil.isEmpty(auxiliaryItemIds)) { + return 0L; + } + + // 2. 计算使用指定辅助核算项目的凭证模板数量 + Set auxiliaryItemIdSet = new HashSet<>(auxiliaryItemIds); + return getVoucherTemplateCountByAuxiliary(accountSetId, + item -> auxiliaryItemIdSet.contains(item.getItemId())); + } + + @Override + public Long getVoucherTemplateCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId) { + return getVoucherTemplateCountByAuxiliary(accountSetId, + item -> ObjUtil.equal(item.getTypeId(), auxiliaryTypeId)); + } + + private Long getVoucherTemplateCountByAuxiliary(Long accountSetId, + Predicate predicate) { + List templates = templateMapper.selectListByAccountSetId(accountSetId); + return (long) filterList(templates, template -> CollUtil.isNotEmpty(template.getEntries()) + && template.getEntries().stream() + .filter(entry -> CollUtil.isNotEmpty(entry.getAuxiliaries())) + .flatMap(entry -> entry.getAuxiliaries().stream()) + .anyMatch(predicate)).size(); + } + + private FmsVoucherTemplateCategoryDO validateTemplateCategory(Long accountSetId, Long id) { + FmsVoucherTemplateCategoryDO category = templateCategoryMapper.selectById(id); + if (category == null || ObjUtil.notEqual(category.getAccountSetId(), accountSetId)) { + throw exception(VOUCHER_TEMPLATE_CATEGORY_NOT_EXISTS); + } + return category; + } + + private void validateTemplateCategoryNameUnique(Long id, Long accountSetId, String name) { + FmsVoucherTemplateCategoryDO category = templateCategoryMapper + .selectByAccountSetIdAndName(accountSetId, name); + if (category != null && ObjUtil.notEqual(category.getId(), id)) { + throw exception(VOUCHER_TEMPLATE_CATEGORY_NAME_DUPLICATE); + } + } + + private FmsVoucherTemplateDO validateVoucherTemplate(Long accountSetId, Long id) { + FmsVoucherTemplateDO template = templateMapper.selectById(id); + if (template == null || ObjUtil.notEqual(template.getAccountSetId(), accountSetId)) { + throw exception(VOUCHER_TEMPLATE_NOT_EXISTS); + } + return template; + } + + private List validateAndBuildTemplateEntries( + Long accountSetId, List entries, Long userId) { + // 1. 批量校验科目 + Map subjectMap = convertMap( + subjectService.getSubjectList(accountSetId, null, userId), FmsSubjectDO::getId); + Set subjectIds = convertSet(entries, FmsVoucherTemplateEntryVO::getSubjectId); + if (!subjectMap.keySet().containsAll(subjectIds)) { + throw exception(SUBJECT_NOT_EXISTS); + } + Set parentSubjectIds = convertSet(subjectMap.values(), FmsSubjectDO::getParentId); + BigDecimal debitAmount = BigDecimal.ZERO; + BigDecimal creditAmount = BigDecimal.ZERO; + boolean saveAmount = false; + for (FmsVoucherTemplateEntryVO entry : entries) { + FmsSubjectDO subject = subjectMap.get(entry.getSubjectId()); + if (ObjUtil.notEqual(subject.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(VOUCHER_SUBJECT_DISABLED); + } + if (parentSubjectIds.contains(subject.getId())) { + throw exception(VOUCHER_SUBJECT_HAS_CHILDREN); + } + BigDecimal entryDebitAmount = ObjUtil.defaultIfNull(entry.getDebitAmount(), BigDecimal.ZERO); + BigDecimal entryCreditAmount = ObjUtil.defaultIfNull(entry.getCreditAmount(), BigDecimal.ZERO); + if (entryDebitAmount.signum() != 0 && entryCreditAmount.signum() != 0) { + throw exception(VOUCHER_ENTRY_AMOUNT_INVALID); + } + BigDecimal entryAmount = entryDebitAmount.abs().max(entryCreditAmount.abs()); + validateTemplateQuantity(entry, subject, entryAmount); + debitAmount = debitAmount.add(entryDebitAmount); + creditAmount = creditAmount.add(entryCreditAmount); + saveAmount = saveAmount || entryAmount.signum() > 0; + } + if (saveAmount && debitAmount.compareTo(creditAmount) != 0) { + throw exception(VOUCHER_AMOUNT_UNBALANCED); + } + + // 2. 批量校验辅助核算类别和项目 + Set auxiliaryItemIds = CollUtil.newHashSet(); + Set auxiliaryTypeIds = CollUtil.newHashSet(); + entries.forEach(entry -> { + if (CollUtil.isNotEmpty(entry.getAuxiliaries())) { + auxiliaryItemIds.addAll(convertSet(entry.getAuxiliaries(), FmsVoucherTemplateEntryVO.AuxiliaryItem::getItemId)); + auxiliaryTypeIds.addAll(convertSet(entry.getAuxiliaries(), FmsVoucherTemplateEntryVO.AuxiliaryItem::getTypeId)); + } + }); + Map auxiliaryItemMap = convertMap( + auxiliaryItemService.validateAuxiliaryItemList(accountSetId, auxiliaryItemIds), + FmsAuxiliaryItemDO::getId); + Map auxiliaryTypeMap = convertMap( + auxiliaryTypeService.validateAuxiliaryTypeList(accountSetId, auxiliaryTypeIds), + FmsAuxiliaryTypeDO::getId); + + // 3. 构造模板 JSON 分录 + return convertList(entries, source -> { + validateTemplateAuxiliaries(source, subjectMap.get(source.getSubjectId())); + FmsVoucherTemplateDO.Entry target = BeanUtils.toBean(source, FmsVoucherTemplateDO.Entry.class); + if (CollUtil.isEmpty(source.getAuxiliaries())) { + target.setAuxiliaries(Collections.emptyList()); + return target; + } + target.setAuxiliaries(convertList(source.getAuxiliaries(), itemSource -> { + FmsAuxiliaryItemDO auxiliaryItem = auxiliaryItemMap.get(itemSource.getItemId()); + if (auxiliaryItem == null + || ObjUtil.notEqual(auxiliaryItem.getAuxiliaryTypeId(), itemSource.getTypeId())) { + throw exception(AUXILIARY_ITEM_NOT_EXISTS); + } + if (ObjUtil.notEqual(auxiliaryItem.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(VOUCHER_AUXILIARY_REQUIRED); + } + return BeanUtils.toBean(itemSource, FmsVoucherEntryDO.AuxiliaryItem.class) + .setType(auxiliaryTypeMap.get(itemSource.getTypeId()).getType()) + .setName(auxiliaryItem.getName()); + })); + return target; + }); + } + + private void validateTemplateQuantity(FmsVoucherTemplateEntryVO entry, + FmsSubjectDO subject, BigDecimal amount) { + BigDecimal quantity = ObjUtil.defaultIfNull(entry.getQuantity(), BigDecimal.ZERO); + BigDecimal unitPrice = ObjUtil.defaultIfNull(entry.getUnitPrice(), BigDecimal.ZERO); + if (Boolean.FALSE.equals(subject.getQuantityAccounting())) { + if (quantity.signum() > 0 || unitPrice.signum() > 0) { + throw exception(VOUCHER_QUANTITY_INVALID); + } + return; + } + if (quantity.signum() == 0 && unitPrice.signum() == 0) { + return; + } + if (quantity.signum() <= 0 || unitPrice.signum() <= 0 + || quantity.multiply(unitPrice).setScale(2, RoundingMode.FLOOR).compareTo(amount) != 0) { + throw exception(VOUCHER_QUANTITY_INVALID); + } + } + + private void validateTemplateAuxiliaries(FmsVoucherTemplateEntryVO entry, FmsSubjectDO subject) { + List auxiliaryTypeIds = ObjUtil.defaultIfNull(subject.getAuxiliaryTypeIds(), Collections.emptyList()); + List auxiliaries = ObjUtil.defaultIfNull(entry.getAuxiliaries(), Collections.emptyList()); + Set requestedTypeIds = convertSet(auxiliaries, FmsVoucherTemplateEntryVO.AuxiliaryItem::getTypeId); + if (auxiliaryTypeIds.size() != auxiliaries.size() + || !requestedTypeIds.containsAll(auxiliaryTypeIds)) { + throw exception(VOUCHER_AUXILIARY_REQUIRED); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordService.java new file mode 100644 index 0000000..421aea8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordService.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword.FmsVoucherWordSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * FMS 凭证字 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsVoucherWordService { + + /** + * 创建凭证字 + * + * @param createReqVO 创建信息 + * @param userId 当前用户编号 + * @return 凭证字编号 + */ + Long createVoucherWord(FmsVoucherWordSaveReqVO createReqVO, Long userId); + + /** + * 修改凭证字 + * + * @param updateReqVO 修改信息 + * @param userId 当前用户编号 + */ + void updateVoucherWord(FmsVoucherWordSaveReqVO updateReqVO, Long userId); + + /** + * 删除凭证字 + * + * @param accountSetId 账套编号 + * @param id 凭证字编号 + * @param userId 当前用户编号 + */ + void deleteVoucherWord(Long accountSetId, Long id, Long userId); + + /** + * 初始化账套默认凭证字 + * + * @param accountSetId 账套编号 + */ + void initializeDefaultVoucherWords(Long accountSetId); + + /** + * 获得账套凭证字列表 + * + * @param accountSetId 账套编号 + * @return 凭证字列表 + */ + List getVoucherWordList(Long accountSetId); + + /** + * 获得账套凭证字 Map + * + * @param accountSetId 账套编号 + * @return 凭证字 Map + */ + default Map getVoucherWordMap(Long accountSetId) { + return convertMap(getVoucherWordList(accountSetId), FmsVoucherWordDO::getId); + } + + /** + * 获得账套凭证字 Map + * + * @param accountSetId 账套编号 + * @return 凭证字 Map,key 为凭证字名称 + */ + default Map getVoucherWordMapByName(Long accountSetId) { + return convertMap(getVoucherWordList(accountSetId), FmsVoucherWordDO::getName); + } + + /** + * 获得账套凭证字列表 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 凭证字列表 + */ + List getVoucherWordList(Long accountSetId, Long userId); + + /** + * 校验并获得凭证字 + * + * @param accountSetId 账套编号 + * @param id 凭证字编号 + * @return 凭证字 + */ + FmsVoucherWordDO validateVoucherWordExists(Long accountSetId, Long id); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordServiceImpl.java new file mode 100644 index 0000000..cdc2fa8 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordServiceImpl.java @@ -0,0 +1,196 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword.FmsVoucherWordSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsVoucherWordMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsVoucherWordPresetEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_WORD_DEFAULT_NOT_DELETABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_WORD_DEFAULT_REQUIRED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_WORD_IN_USE_NOT_DELETABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_WORD_IN_USE_NOT_EDITABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_WORD_IN_USE_BY_CLOSING_SCHEME_NOT_DELETABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_WORD_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_WORD_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_VOUCHER_WORD_CREATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_VOUCHER_WORD_CREATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_VOUCHER_WORD_DELETE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_VOUCHER_WORD_DELETE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_VOUCHER_WORD_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_VOUCHER_WORD_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_VOUCHER_WORD_UPDATE_SUCCESS; + +/** + * FMS 凭证字 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsVoucherWordServiceImpl implements FmsVoucherWordService { + + @Resource + private FmsVoucherWordMapper voucherWordMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + @Lazy // 懒加载,避免循环依赖 + private FmsVoucherService voucherService; + @Resource + @Lazy // 懒加载,避免循环依赖 + private FmsClosingSchemeService closingSchemeService; + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_WORD_TYPE, subType = FMS_VOUCHER_WORD_CREATE_SUB_TYPE, + bizNo = "{{#voucherWordId}}", success = FMS_VOUCHER_WORD_CREATE_SUCCESS) + public Long createVoucherWord(FmsVoucherWordSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 校验凭证字名称唯一 + validateVoucherWordNameUnique(null, createReqVO.getAccountSetId(), createReqVO.getName()); + // 1.3 确定默认状态,并清除原默认凭证字 + FmsVoucherWordDO defaultVoucherWord = voucherWordMapper.selectDefaultByAccountSetId( + createReqVO.getAccountSetId()); + boolean defaultStatus = Boolean.TRUE.equals(createReqVO.getDefaultStatus()) + || defaultVoucherWord == null; + if (defaultStatus && defaultVoucherWord != null) { + voucherWordMapper.updateById(new FmsVoucherWordDO().setId(defaultVoucherWord.getId()) + .setDefaultStatus(false)); + } + + // 2. 创建凭证字 + FmsVoucherWordDO lastVoucherWord = voucherWordMapper.selectLastByAccountSetId(createReqVO.getAccountSetId()); + FmsVoucherWordDO voucherWord = BeanUtils.toBean(createReqVO, FmsVoucherWordDO.class) + .setId(null).setDefaultStatus(defaultStatus) + .setSort(lastVoucherWord == null ? 1 : lastVoucherWord.getSort() + 1); + voucherWordMapper.insert(voucherWord); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("voucherWordId", voucherWord.getId()); + return voucherWord.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_WORD_TYPE, subType = FMS_VOUCHER_WORD_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_VOUCHER_WORD_UPDATE_SUCCESS) + public void updateVoucherWord(FmsVoucherWordSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验凭证字存在、使用状态和名称唯一 + FmsVoucherWordDO voucherWord = validateVoucherWordExists(updateReqVO.getAccountSetId(), updateReqVO.getId()); + boolean contentChanged = ObjUtil.notEqual(voucherWord.getName(), updateReqVO.getName()) + || ObjUtil.notEqual(voucherWord.getPrintTitle(), updateReqVO.getPrintTitle()); + if (contentChanged && voucherService.getVoucherCountByVoucherWordId( + updateReqVO.getAccountSetId(), voucherWord.getId()) > 0) { + throw exception(VOUCHER_WORD_IN_USE_NOT_EDITABLE); + } + validateVoucherWordNameUnique(voucherWord.getId(), updateReqVO.getAccountSetId(), updateReqVO.getName()); + + // 1.3 校验并切换默认凭证字 + if (Boolean.TRUE.equals(voucherWord.getDefaultStatus()) + && Boolean.FALSE.equals(updateReqVO.getDefaultStatus())) { + throw exception(VOUCHER_WORD_DEFAULT_REQUIRED); + } + if (Boolean.TRUE.equals(updateReqVO.getDefaultStatus())) { + FmsVoucherWordDO defaultVoucherWord = voucherWordMapper.selectDefaultByAccountSetId( + updateReqVO.getAccountSetId()); + if (defaultVoucherWord != null && ObjUtil.notEqual(defaultVoucherWord.getId(), voucherWord.getId())) { + voucherWordMapper.updateById(new FmsVoucherWordDO().setId(defaultVoucherWord.getId()) + .setDefaultStatus(false)); + } + } + + // 2. 更新凭证字 + voucherWordMapper.updateById(new FmsVoucherWordDO().setId(voucherWord.getId()) + .setName(updateReqVO.getName()).setPrintTitle(updateReqVO.getPrintTitle()) + .setDefaultStatus(updateReqVO.getDefaultStatus())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_WORD_TYPE, subType = FMS_VOUCHER_WORD_DELETE_SUB_TYPE, + bizNo = "{{#id}}", success = FMS_VOUCHER_WORD_DELETE_SUCCESS) + public void deleteVoucherWord(Long accountSetId, Long id, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验凭证字存在且不是默认凭证字 + FmsVoucherWordDO voucherWord = validateVoucherWordExists(accountSetId, id); + if (Boolean.TRUE.equals(voucherWord.getDefaultStatus())) { + throw exception(VOUCHER_WORD_DEFAULT_NOT_DELETABLE); + } + // 1.3 校验凭证字未被凭证使用 + Long voucherCount = voucherService.getVoucherCountByVoucherWordId(accountSetId, id); + if (voucherCount > 0) { + throw exception(VOUCHER_WORD_IN_USE_NOT_DELETABLE, voucherCount); + } + // 1.4 校验凭证字未被结账方案使用 + Long closingSchemeCount = closingSchemeService.getClosingSchemeCountByVoucherWordId(accountSetId, id); + if (closingSchemeCount > 0) { + throw exception(VOUCHER_WORD_IN_USE_BY_CLOSING_SCHEME_NOT_DELETABLE, closingSchemeCount); + } + + // 2. 删除凭证字 + voucherWordMapper.deleteById(id); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("voucherWord", voucherWord); + } + + @Override + public void initializeDefaultVoucherWords(Long accountSetId) { + List list = convertList(FmsVoucherWordPresetEnum.values(), preset -> + new FmsVoucherWordDO().setAccountSetId(accountSetId).setName(preset.getName()) + .setPrintTitle(preset.getPrintTitle()).setDefaultStatus(preset.getDefaultStatus()) + .setSort(preset.getSort())); + voucherWordMapper.insertBatch(list); + } + + @Override + public List getVoucherWordList(Long accountSetId) { + return voucherWordMapper.selectListByAccountSetId(accountSetId); + } + + @Override + public List getVoucherWordList(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询凭证字列表 + return getVoucherWordList(accountSetId); + } + + @Override + public FmsVoucherWordDO validateVoucherWordExists(Long accountSetId, Long id) { + FmsVoucherWordDO voucherWord = voucherWordMapper.selectById(id); + if (voucherWord == null || ObjUtil.notEqual(voucherWord.getAccountSetId(), accountSetId)) { + throw exception(VOUCHER_WORD_NOT_EXISTS); + } + return voucherWord; + } + + private void validateVoucherWordNameUnique(Long id, Long accountSetId, String name) { + FmsVoucherWordDO voucherWord = voucherWordMapper.selectByAccountSetIdAndName(accountSetId, name); + if (voucherWord != null && ObjUtil.notEqual(voucherWord.getId(), id)) { + throw exception(VOUCHER_WORD_NAME_DUPLICATE); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeService.java new file mode 100644 index 0000000..ea1c8ab --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeService.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.fms.service.home; + +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeMetricDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeRespVO; + +/** + * FMS 首页 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsHomeService { + + /** + * 获得首页 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 首页信息 + */ + FmsHomeRespVO getHome(Long accountSetId, Long userId); + + /** + * 获得财务指标明细 + * + * @param accountSetId 账套编号 + * @param metricKey 指标标识 + * @param userId 当前用户编号 + * @return 财务指标明细 + */ + FmsHomeMetricDetailRespVO getMetricDetail(Long accountSetId, String metricKey, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeServiceImpl.java new file mode 100644 index 0000000..dcc870c --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeServiceImpl.java @@ -0,0 +1,444 @@ +package cn.iocoder.yudao.module.fms.service.home; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeMetricDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceIndicatorDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.enums.config.FmsFinanceIndicatorTypeEnum; +import cn.iocoder.yudao.module.fms.enums.home.FmsHomeMetricEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceIndicatorService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.report.FmsBalanceSheetService; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import cn.iocoder.yudao.module.fms.service.report.FmsReportCommonService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_NOT_INITIALIZED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.HOME_METRIC_INVALID; + +/** + * FMS 首页 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsHomeServiceImpl implements FmsHomeService { + + private static final int TREND_MONTH_COUNT = 12; + private static final Pattern LINE_FORMULA_PATTERN = Pattern.compile("([+-]?)L(\\d+)"); + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsClosingPeriodService closingPeriodService; + @Resource + private FmsIncomeStatementService incomeStatementService; + @Resource + private FmsBalanceSheetService balanceSheetService; + @Resource + private FmsReportCommonService reportCommonService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsFinanceIndicatorService financeIndicatorService; + + @Override + public FmsHomeRespVO getHome(Long accountSetId, Long userId) { + // 1.1 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 校验账套初始化状态 + if (!Boolean.TRUE.equals(accountSet.getInitialized()) || accountSet.getStartTime() == null) { + throw exception(ACCOUNT_SET_NOT_INITIALIZED); + } + // 1.3 确定当前会计期间 + YearMonth currentMonth = closingPeriodService.getCurrentMonth(accountSetId, accountSet.getStartTime()); + YearMonth startMonth = YearMonth.from(accountSet.getStartTime()); + List indicators = getEnabledIndicators(accountSetId, userId); + + // 2. 计算最近十二期财务指标 + List trends = new ArrayList<>(); + for (int index = TREND_MONTH_COUNT - 1; index >= 0; index--) { + YearMonth month = currentMonth.minusMonths(index); + trends.add(month.isBefore(startMonth) ? buildEmptyTrend(month) + : buildTrend(accountSetId, month, indicators, userId)); + } + + // 3. 返回当期指标和趋势 + FmsHomeRespVO.Trend currentTrend = CollUtil.getLast(trends); + return new FmsHomeRespVO().setCurrentMonth(currentMonth.toString()) + .setMetrics(currentTrend.getMetrics()).setTrends(trends); + } + + @Override + public FmsHomeMetricDetailRespVO getMetricDetail( + Long accountSetId, String metricKey, Long userId) { + // 1.1 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 校验财务指标 + FmsFinanceIndicatorDO metric = CollUtil.findOne( + getEnabledIndicators(accountSetId, userId), + item -> StrUtil.equals(item.getCode(), metricKey)); + if (metric == null) throw exception(HOME_METRIC_INVALID); + // 1.3 校验账套初始化状态 + if (!Boolean.TRUE.equals(accountSet.getInitialized()) || accountSet.getStartTime() == null) { + throw exception(ACCOUNT_SET_NOT_INITIALIZED); + } + // 1.4 确定当前会计期间 + YearMonth currentMonth = closingPeriodService.getCurrentMonth(accountSetId, accountSet.getStartTime()); + YearMonth startMonth = YearMonth.from(accountSet.getStartTime()); + + // 2. 计算最近十二期指标趋势 + List trends = new ArrayList<>(); + List currentItems = new ArrayList<>(); + List currentStructure = new ArrayList<>(); + for (int index = TREND_MONTH_COUNT - 1; index >= 0; index--) { + YearMonth month = currentMonth.minusMonths(index); + BigDecimal amount = BigDecimal.ZERO; + if (!month.isBefore(startMonth)) { + amount = calculateIndicator(metric, accountSetId, month, userId); + if (month.equals(currentMonth)) { + // TODO DONE @AI:行次公式统一复用报表公共判断能力。 + // TODO DONE @AI:报表查询结果先取得,再转换为首页明细结构。 + if (reportCommonService.isLineFormula(metric.getFormula())) { + if (FmsFinanceIndicatorTypeEnum.isIncomeStatement(metric.getType())) { + currentItems = getIncomeStatement(accountSetId, month, userId); + } else { + // 先查询资产负债表行次数据,再转换为首页明细项目 + FmsReportListReqVO reqVO = new FmsReportListReqVO().setAccountSetId(accountSetId) + .setStartMonth(month.toString()).setEndMonth(month.toString()); + Map lineMap = balanceSheetService.getBalanceSheetLineMap( + reqVO, month, month, userId); + currentItems = new ArrayList<>(lineMap.values()); + } + } else { + currentStructure = buildSubjectStructure(metric.getFormula(), accountSetId, month, userId, + FmsFinanceIndicatorTypeEnum.isIncomeStatement(metric.getType())); + } + } + } + trends.add(new FmsHomeMetricDetailRespVO.Trend() + .setMonth(month.toString()).setAmount(amount)); + } + + // 3. 返回趋势和当期科目构成 + return new FmsHomeMetricDetailRespVO().setKey(metric.getCode()).setName(metric.getName()) + .setTrends(trends).setStructure(CollUtil.isNotEmpty(currentStructure) + ? currentStructure : buildStructure(metric.getFormula(), currentItems)); + } + + /** + * 构建指定会计期间的首页财务指标趋势 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 当前用户编号 + * @return 首页财务指标趋势 + */ + private FmsHomeRespVO.Trend buildTrend(Long accountSetId, YearMonth month, + List indicators, Long userId) { + boolean hasIncome = CollUtil.findOne(indicators, + item -> FmsFinanceIndicatorTypeEnum.isIncomeStatement(item.getType())) != null; + boolean hasBalance = CollUtil.findOne(indicators, + item -> FmsFinanceIndicatorTypeEnum.isBalanceSheet(item.getType())) != null; + Map incomeLineMap = hasIncome + ? convertMap(getIncomeStatement(accountSetId, month, userId), FmsReportItemRespVO::getRowNo) + : Collections.emptyMap(); + Map balanceLineMap = hasBalance + ? balanceSheetService.getBalanceSheetLineMap(new FmsReportListReqVO().setAccountSetId(accountSetId) + .setStartMonth(month.toString()).setEndMonth(month.toString()), month, month, userId) + : Collections.emptyMap(); + List metrics = convertList(indicators, indicator -> + new FmsHomeRespVO.Metric().setKey(indicator.getCode()).setName(indicator.getName()) + .setAmount(reportCommonService.isLineFormula(indicator.getFormula()) + ? calculateIndicator(indicator, incomeLineMap, balanceLineMap) + : calculateIndicator(indicator, accountSetId, month, userId))); + return new FmsHomeRespVO.Trend().setMonth(month.toString()).setMetrics(metrics) + .setIncome(findAmount(metrics, "income")).setOperatingCost(findAmount(metrics, "operatingCost")) + .setProfit(findAmount(metrics, "profit")).setExpense(findAmount(metrics, "expense")) + .setOther(findAmount(metrics, "other")); + } + + /** + * 获得指定会计期间的利润表 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 当前用户编号 + * @return 利润表项目数组 + */ + private List getIncomeStatement(Long accountSetId, YearMonth month, Long userId) { + // TODO DONE @AI:查询参数先构建,再执行查询。 + FmsReportListReqVO reqVO = new FmsReportListReqVO().setAccountSetId(accountSetId) + .setStartMonth(month.toString()).setEndMonth(month.toString()); + return incomeStatementService.getIncomeStatement(reqVO, userId); + } + + /** + * 构建未启用会计期间的空趋势 + * + * @param month 会计期间 + * @return 空趋势 + */ + private FmsHomeRespVO.Trend buildEmptyTrend(YearMonth month) { + return new FmsHomeRespVO.Trend().setMonth(month.toString()) + .setMetrics(new ArrayList<>()) + .setIncome(BigDecimal.ZERO).setOperatingCost(BigDecimal.ZERO).setProfit(BigDecimal.ZERO) + .setExpense(BigDecimal.ZERO).setOther(BigDecimal.ZERO); + } + + /** + * 根据指标公式计算首页财务指标 + * + * @param indicator 财务指标 + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param userId 当前用户编号 + * @return 指标金额 + */ + private BigDecimal calculateIndicator(FmsFinanceIndicatorDO indicator, Long accountSetId, YearMonth month, + Long userId) { + // 1. 准备当前会计期间的报表查询条件 + FmsReportListReqVO reqVO = new FmsReportListReqVO().setAccountSetId(accountSetId) + .setStartMonth(month.toString()).setEndMonth(month.toString()); + // 2. 查询指标所属报表的行次数据 + Map lineMap; + if (FmsFinanceIndicatorTypeEnum.isBalanceSheet(indicator.getType())) { + lineMap = balanceSheetService.getBalanceSheetLineMap(reqVO, month, month, userId); + } else { + lineMap = convertMap(getIncomeStatement(accountSetId, month, userId), FmsReportItemRespVO::getRowNo); + } + // 3. 行次公式直接按报表行次汇总 + if (reportCommonService.isLineFormula(indicator.getFormula())) { + return calculateLineFormula(indicator.getFormula(), lineMap, + FmsFinanceIndicatorTypeEnum.isIncomeStatement(indicator.getType())); + } + // 4. 科目公式查询余额和科目,并按公式规则计算 + Map balanceMap = reportCommonService.getSubjectBalanceMap(accountSetId, month, month, userId); + Map subjectMap = convertMap(subjectService.getSubjectList(accountSetId, null, userId), FmsSubjectDO::getId); + BigDecimal result = BigDecimal.ZERO; + for (FmsReportFormulaRespVO formula : reportCommonService.parseSubjectFormula(indicator.getFormula())) { + FmsSubjectDO subject = subjectMap.get(formula.getSubjectId()); + BigDecimal amount = FmsFinanceIndicatorTypeEnum.isBalanceSheet(indicator.getType()) + ? reportCommonService.calculateBalanceAmount(formula.getRules(), subject, balanceMap, + subjectMap, false) + : reportCommonService.calculateIncomeOccurrenceAmount(formula.getRules(), subject, + balanceMap.get(formula.getSubjectId()), true, formula.getOperator()); + result = reportCommonService.applyOperator(result, amount, formula.getOperator()); + } + return result; + } + + private BigDecimal calculateIndicator(FmsFinanceIndicatorDO indicator, + Map incomeLineMap, + Map balanceLineMap) { + Map lineMap = FmsFinanceIndicatorTypeEnum.isBalanceSheet(indicator.getType()) + ? balanceLineMap : incomeLineMap; + return reportCommonService.isLineFormula(indicator.getFormula()) + ? calculateLineFormula(indicator.getFormula(), lineMap, + FmsFinanceIndicatorTypeEnum.isIncomeStatement(indicator.getType())) + : BigDecimal.ZERO; + } + + private BigDecimal findAmount(List metrics, String key) { + FmsHomeRespVO.Metric metric = CollUtil.findOne(metrics, item -> StrUtil.equals(item.getKey(), key)); + return metric == null ? BigDecimal.ZERO : NumberUtils.zeroIfNull(metric.getAmount()); + } + + /** + * 查询账套下已启用的首页财务指标;历史账套未初始化指标时回退到原有五项指标 + * + * @param accountSetId 账套编号 + * @param userId 当前用户编号 + * @return 已启用的首页财务指标 + */ + private List getEnabledIndicators(Long accountSetId, Long userId) { + List indicators = financeIndicatorService.getEnabledFinanceIndicatorList(accountSetId, + userId); + if (CollUtil.isNotEmpty(indicators)) { + return indicators; + } + // 兼容尚未执行新初始化流程的历史账套,保留原有五项首页指标 + return convertList(Arrays.asList(FmsHomeMetricEnum.values()), metric -> new FmsFinanceIndicatorDO() + .setCode(metric.getKey()).setName(metric.getName()) + .setType(FmsFinanceIndicatorTypeEnum.INCOME_STATEMENT.getType()) + .setFormula(JsonUtils.toJsonString(Collections.singletonList(metric.getStructureFormula()))) + .setSort(metric.ordinal()).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAccountSetId(accountSetId)); + } + + /** + * 计算报表行次公式 + * + * @param formula 指标公式 JSON + * @param lineMap 报表行次映射 + * @param current 是否取本期发生额 + * @return 公式计算结果 + */ + private BigDecimal calculateLineFormula(String formula, Map lineMap, boolean current) { + List expressions = JsonUtils.parseArray(formula, String.class); + String expression = CollUtil.getFirst(expressions); + if (StrUtil.isEmpty(expression)) { + return BigDecimal.ZERO; + } + BigDecimal result = BigDecimal.ZERO; + Matcher matcher = LINE_FORMULA_PATTERN.matcher(expression); + while (matcher.find()) { + FmsReportItemRespVO item = lineMap.get(Integer.valueOf(matcher.group(2))); + BigDecimal amount = item == null ? BigDecimal.ZERO : current + ? NumberUtils.zeroIfNull(item.getCurrentAmount()) + : NumberUtils.zeroIfNull(item.getClosingAmount()); + result = StrUtil.equals(matcher.group(1), "-") ? result.subtract(amount) : result.add(amount); + } + return result; + } + + /** + * 构建指定财务指标的当期科目构成 + * + * @param formula 指标公式 + * @param items 当期报表项目数组 + * @return 科目构成数组 + */ + private List buildStructure(String formula, List items) { + // 1. 报表无当期项目时,不生成科目构成 + if (CollUtil.isEmpty(items)) { + return new ArrayList<>(); + } + + // 2. 建立有效报表行次映射 + Map itemMap = convertMap( + filterList(items, item -> item.getRowNo() != null && item.getRowNo() > 0), + FmsReportItemRespVO::getRowNo); + + // 3. 递归解析指标行次公式,按科目汇总当期金额 + Map structureMap = new LinkedHashMap<>(); + collectLineFormula(formula, BigDecimal.ONE, itemMap, structureMap, new HashSet<>()); + + // 4. 仅保留正数科目构成,并按金额从大到小排列 + List result = filterList(structureMap.values(), + item -> item.getAmount().signum() > 0); + result.sort(Comparator.comparing(FmsHomeMetricDetailRespVO.Structure::getAmount).reversed()); + return result; + } + + private List buildSubjectStructure(String formula, Long accountSetId, + YearMonth month, Long userId, boolean income) { + Map balanceMap = reportCommonService.getSubjectBalanceMap(accountSetId, month, month, userId); + Map subjectMap = convertMap(subjectService.getSubjectList(accountSetId, null, userId), FmsSubjectDO::getId); + Map structureMap = new LinkedHashMap<>(); + for (FmsReportFormulaRespVO item : reportCommonService.parseSubjectFormula(formula)) { + FmsSubjectDO subject = subjectMap.get(item.getSubjectId()); + BigDecimal amount = income + ? reportCommonService.calculateIncomeOccurrenceAmount(item.getRules(), subject, balanceMap.get(item.getSubjectId()), true, item.getOperator()) + : reportCommonService.calculateBalanceAmount(item.getRules(), subject, balanceMap, subjectMap, false); + if (StrUtil.equals(item.getOperator(), "-")) { + amount = amount.negate(); + } + FmsHomeMetricDetailRespVO.Structure structure = structureMap.computeIfAbsent(item.getSubjectId(), + subjectId -> new FmsHomeMetricDetailRespVO.Structure().setSubjectId(subjectId) + .setSubjectCode(subject == null ? item.getSubjectNumber() : subject.getCode()) + .setSubjectName(subject == null ? item.getSubjectName() : subject.getName()) + .setAmount(BigDecimal.ZERO)); + structure.setAmount(structure.getAmount().add(amount)); + } + List result = filterList(structureMap.values(), item -> item.getAmount().signum() > 0); + result.sort(Comparator.comparing(FmsHomeMetricDetailRespVO.Structure::getAmount).reversed()); + return result; + } + + /** + * 递归解析行次公式并汇总科目构成 + * + * @param expression 行次公式 + * @param factor 金额系数 + * @param itemMap 行次及利润表项目的映射 + * @param structureMap 科目编号及科目构成的映射 + * @param visitingRowNumbers 当前递归链路中的行次集合 + */ + private void collectLineFormula(String expression, BigDecimal factor, Map itemMap, + Map structureMap, Set visitingRowNumbers) { + Matcher matcher = LINE_FORMULA_PATTERN.matcher(expression); + while (matcher.find()) { + BigDecimal rowFactor = StrUtil.equals(matcher.group(1), "-") ? factor.negate() : factor; + collectRowStructure(Integer.valueOf(matcher.group(2)), rowFactor, itemMap, structureMap, visitingRowNumbers); + } + } + + /** + * 解析利润表行的公式并汇总科目构成 + * + * @param rowNo 行次 + * @param factor 金额系数 + * @param itemMap 行次及利润表项目的映射 + * @param structureMap 科目编号及科目构成的映射 + * @param visitingRowNumbers 当前递归链路中的行次集合 + */ + private void collectRowStructure(Integer rowNo, BigDecimal factor, + Map itemMap, + Map structureMap, + Set visitingRowNumbers) { + FmsReportItemRespVO item = itemMap.get(rowNo); + if (item == null || StrUtil.isEmpty(item.getFormula()) || !visitingRowNumbers.add(rowNo)) { + return; + } + try { + if (item.getFormula().contains("L")) { + List expressions = JsonUtils.parseArray(item.getFormula(), String.class); + if (CollUtil.isNotEmpty(expressions)) { + collectLineFormula(CollUtil.getFirst(expressions), factor, itemMap, structureMap, visitingRowNumbers); + } + return; + } + for (FmsReportFormulaRespVO formula : JsonUtils.parseArray(item.getFormula(), FmsReportFormulaRespVO.class)) { + if (formula.getSubjectId() == null) { + continue; + } + BigDecimal amount = NumberUtils.zeroIfNull(formula.getCurrentAmount()).multiply(factor); + if (StrUtil.equals(formula.getOperator(), "-")) { + amount = amount.negate(); + } + FmsHomeMetricDetailRespVO.Structure structure = structureMap.computeIfAbsent( + formula.getSubjectId(), subjectId -> new FmsHomeMetricDetailRespVO.Structure() + .setSubjectId(subjectId).setSubjectCode(formula.getSubjectNumber()) + .setSubjectName(formula.getSubjectName()).setAmount(BigDecimal.ZERO)); + structure.setAmount(structure.getAmount().add(amount)); + } + } finally { + visitingRowNumbers.remove(rowNo); + } + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerService.java new file mode 100644 index 0000000..7e42a69 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerService.java @@ -0,0 +1,155 @@ +package cn.iocoder.yudao.module.fms.service.ledger; + +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerAuxiliaryListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerEntryVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarybalance.FmsLedgerAuxiliaryBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarydetail.FmsLedgerAuxiliaryDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.general.FmsLedgerGeneralRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.multicolumn.FmsLedgerMultiColumnRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitydetail.FmsLedgerQuantityDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitygeneral.FmsLedgerQuantityGeneralRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +/** + * FMS 账簿 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsLedgerService { + + // ==================== 公共查询 ==================== + + /** + * 获得指定期间的账簿凭证分录列表 + * + * @param accountSetId 账套编号 + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @param userId 用户编号 + * @return 凭证分录列表 + */ + List getEntryList(Long accountSetId, LocalDateTime beginTime, + LocalDateTime endTime, Long userId); + + /** + * 获得指定辅助核算组合的余额 + * + * @param accountSetId 账套编号 + * @param month 会计期间 + * @param subjectId 科目编号 + * @param auxiliaryItemIds 辅助核算项目编号数组 + * @param userId 用户编号 + * @return 借方为正、贷方为负的余额 + */ + BigDecimal getAuxiliaryCombinationBalance(Long accountSetId, String month, Long subjectId, + Collection auxiliaryItemIds, Long userId); + + // ==================== 明细账 ==================== + + /** + * 获得明细账列表 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 明细账列表 + */ + List getDetailList(FmsLedgerListReqVO listReqVO, Long userId); + + /** + * 获得指定期间有发生额的明细账科目树 + * + * @param listReqVO 查询参数 + * @param userId 用户编号 + * @return 科目列表 + */ + List getDetailSubjectList(FmsLedgerListReqVO listReqVO, Long userId); + + // ==================== 总账 ==================== + + /** + * 获得总账列表 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 总账列表 + */ + List getGeneralList(FmsLedgerListReqVO listReqVO, Long userId); + + // ==================== 科目余额表 ==================== + + /** + * 获得科目余额表列表 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 科目余额表列表 + */ + List getSubjectBalanceList( + FmsLedgerListReqVO listReqVO, Long userId); + + // ==================== 多栏账 ==================== + + /** + * 获得多栏账 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 多栏账 + */ + FmsLedgerMultiColumnRespVO getMultiColumn(FmsLedgerListReqVO listReqVO, Long userId); + + // ==================== 核算项目明细账 ==================== + + /** + * 获得核算项目明细账列表 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 核算项目明细账列表 + */ + List getAuxiliaryDetailList( + FmsLedgerAuxiliaryListReqVO listReqVO, Long userId); + + // ==================== 核算项目余额表 ==================== + + /** + * 获得核算项目余额表列表 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 核算项目余额表列表 + */ + List getAuxiliaryBalanceList( + FmsLedgerAuxiliaryListReqVO listReqVO, Long userId); + + // ==================== 数量金额明细账 ==================== + + /** + * 获得数量金额明细账列表 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 数量金额明细账列表 + */ + List getQuantityDetailList(FmsLedgerListReqVO listReqVO, Long userId); + + // ==================== 数量金额总账 ==================== + + /** + * 获得数量金额总账列表 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 数量金额总账列表 + */ + List getQuantityGeneralList(FmsLedgerListReqVO listReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerServiceImpl.java new file mode 100644 index 0000000..15feee0 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerServiceImpl.java @@ -0,0 +1,1850 @@ +package cn.iocoder.yudao.module.fms.service.ledger; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerAuxiliaryListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerEntryVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarybalance.FmsLedgerAuxiliaryBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarydetail.FmsLedgerAuxiliaryDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.general.FmsLedgerGeneralRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.multicolumn.FmsLedgerMultiColumnRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitydetail.FmsLedgerQuantityDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitygeneral.FmsLedgerQuantityGeneralRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsInitialBalanceDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsInitialBalanceMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.ledger.FmsLedgerQueryMapper; +import cn.iocoder.yudao.module.fms.enums.common.FmsBalanceDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryItemService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sumBigDecimal; +import static cn.iocoder.yudao.framework.common.util.number.NumberUtils.zeroIfNull; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.AUXILIARY_ITEM_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.LEDGER_PERIOD_BEFORE_ACCOUNT_START; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.LEDGER_PERIOD_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.VOUCHER_AUXILIARY_REQUIRED; + +/** + * FMS 账簿 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsLedgerServiceImpl implements FmsLedgerService { + + @Resource + private FmsLedgerQueryMapper ledgerQueryMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsFinanceParameterService financeParameterService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsAuxiliaryItemService auxiliaryItemService; + @Resource + private FmsInitialBalanceMapper initialBalanceMapper; + + // ==================== 公共查询 ==================== + + @Override + public List getEntryList(Long accountSetId, LocalDateTime beginTime, + LocalDateTime endTime, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询指定期间的凭证分录 + return filterList(ledgerQueryMapper.selectEntryListBeforeTime(accountSetId, endTime), + entry -> !entry.getVoucherTime().isBefore(beginTime)); + } + + @Override + public BigDecimal getAuxiliaryCombinationBalance(Long accountSetId, String month, Long subjectId, + Collection auxiliaryItemIds, Long userId) { + // 1. 加载账簿上下文 + LedgerContext context = buildContext(new FmsLedgerListReqVO().setAccountSetId(accountSetId) + .setStartMonth(month).setEndMonth(month), userId); + + // 2. 校验科目和辅助核算项目 + FmsSubjectDO subject = context.subjectMap.get(subjectId); + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + List auxiliaryItems = auxiliaryItemService + .validateAuxiliaryItemList(accountSetId, auxiliaryItemIds); + Set auxiliaryItemIdSet = new LinkedHashSet<>(auxiliaryItemIds); + if (CollUtil.size(subject.getAuxiliaryTypeIds()) != auxiliaryItemIdSet.size() + || !new LinkedHashSet<>(subject.getAuxiliaryTypeIds()).equals( + convertSet(auxiliaryItems, FmsAuxiliaryItemDO::getAuxiliaryTypeId))) { + throw exception(VOUCHER_AUXILIARY_REQUIRED); + } + + // 3. 计算辅助核算组合的初始余额 + BigDecimal signedBalance = BigDecimal.ZERO; + List initialBalances = filterList(context.initialBalances, + initial -> ObjUtil.equal(initial.getSubjectId(), subjectId)); + for (FmsInitialBalanceDO initial : initialBalances) { + if (CollUtil.isEmpty(initial.getAssistBalances())) { + continue; + } + List assistBalances = filterList(initial.getAssistBalances(), + assist -> CollUtil.isNotEmpty(assist.getAuxiliaries()) + && auxiliaryItemIdSet.equals(convertSet(assist.getAuxiliaries(), + FmsInitialBalanceDO.AuxiliaryItem::getItemId))); + BigDecimal initialAmount = sumBigDecimal(assistBalances, + FmsInitialBalanceDO.AssistBalance::getOpeningAmount); + signedBalance = signedBalance.add(ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()) ? initialAmount : initialAmount.negate()); + } + + // 4. 累加会计期间结束前的凭证发生额 + List entries = filterList(context.entries, + entry -> ObjUtil.equal(entry.getSubjectId(), subjectId) + && CollUtil.isNotEmpty(entry.getAuxiliaries()) + && auxiliaryItemIdSet.equals(convertSet(entry.getAuxiliaries(), + FmsVoucherEntryDO.AuxiliaryItem::getItemId))); + return signedBalance.add(calculateSignedAmount(entries, null, + context.endMonth.plusMonths(1).atDay(1).atStartOfDay())); + } + + // ==================== 明细账 ==================== + + @Override + public List getDetailList(FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 加载账簿上下文 + LedgerContext context = buildContext(listReqVO, userId); + // 2. 校验科目 + FmsSubjectDO subject = context.subjectMap.get(listReqVO.getSubjectId()); + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + + // 3. 计算明细账 + return buildLedgerRows(context, subject, true); + } + + @Override + public List getDetailSubjectList(FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 加载指定期间的账簿上下文 + LedgerContext context = buildContext(listReqVO, userId); + LocalDateTime beginTime = context.startMonth.atDay(1).atStartOfDay(); + LocalDateTime endTime = context.endMonth.plusMonths(1).atDay(1).atStartOfDay(); + + // 2. 收集期间有凭证发生额的科目,并补齐其父级节点 + Set subjectIds = filterEntriesByTime(context.entries, beginTime, endTime).stream() + .map(FmsLedgerEntryVO::getSubjectId).collect(Collectors.toCollection(LinkedHashSet::new)); + Set visibleSubjectIds = new LinkedHashSet<>(subjectIds); + for (FmsSubjectDO subject : context.subjects) { + if (!subjectIds.contains(subject.getId())) { + continue; + } + // TODO @AI:尽量不要 while true 死循环,而是有一个 for i 《 short 的上限》 + Long parentId = subject.getParentId(); + while (parentId != null && visibleSubjectIds.add(parentId)) { + FmsSubjectDO parent = context.subjectMap.get(parentId); + parentId = parent == null ? null : parent.getParentId(); + } + } + return filterList(context.subjects, subject -> visibleSubjectIds.contains(subject.getId())); + } + + // ==================== 总账 ==================== + + @Override + public List getGeneralList(FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 加载账簿上下文 + LedgerContext context = buildContext(listReqVO, userId); + + // 2. 筛选总账科目 + List subjects = filterSubjects(context, listReqVO); + + // 3. 计算总账 + List detailRows = new ArrayList<>(); + for (FmsSubjectDO subject : subjects) { + List rows = buildLedgerRows(context, subject, false); + if (CollUtil.isNotEmpty(rows) + && rows.stream().anyMatch(row -> row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_VOUCHER) + || zeroIfNull(row.getDebitAmount()).signum() != 0 + || zeroIfNull(row.getCreditAmount()).signum() != 0 + || zeroIfNull(row.getBalance()).signum() != 0)) { + detailRows.addAll(rows); + } + } + return BeanUtils.toBean(detailRows, FmsLedgerGeneralRespVO.class); + } + + // ==================== 科目余额表 ==================== + + @Override + public List getSubjectBalanceList( + FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 加载账簿上下文 + LedgerContext context = buildContext(listReqVO, userId); + + // 2. 计算辅助核算组合余额 + Map auxiliaryItemMap = convertMap( + auxiliaryItemService.getAuxiliaryItemListByAccountSetId(listReqVO.getAccountSetId(), userId), + FmsAuxiliaryItemDO::getId); + Map> auxiliaryBalanceMap = + buildAuxiliaryCombinationBalanceMap(context, auxiliaryItemMap); + + // 3. 计算科目余额并过滤八项金额全零且没有辅助组合余额的科目 + List result = buildSubjectBalanceList(context, listReqVO, + balance -> hasSubjectBalanceAmount(balance) + || auxiliaryBalanceMap.containsKey(balance.getSubjectId())); + appendAuxiliaryCombinationBalances(result, auxiliaryBalanceMap); + return result; + } + + // ==================== 多栏账 ==================== + + @Override + public FmsLedgerMultiColumnRespVO getMultiColumn(FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 加载账簿上下文 + LedgerContext context = buildContext(listReqVO, userId); + // 2. 校验科目 + FmsSubjectDO subject = context.subjectMap.get(listReqVO.getSubjectId()); + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + + // 3. 筛选有数据的动态科目列 + Set subjectIds = collectSubjectIds(context, subject.getId()); + List columns = filterList(context.subjects, column -> subjectIds.contains(column.getId()) + && (context.entries.stream().anyMatch(entry -> ObjUtil.equal(entry.getSubjectId(), column.getId())) + || context.initialBalances.stream().anyMatch(initial -> ObjUtil.equal( + initial.getSubjectId(), column.getId()) + && (zeroIfNull(initial.getOpeningAmount()).signum() != 0 + || zeroIfNull(initial.getYearDebitAmount()).signum() != 0 + || zeroIfNull(initial.getYearCreditAmount()).signum() != 0)))); + + // 4. 计算多栏账 + List rows = appendMultiColumnEndingRows( + buildLedgerRows(context, subject, true)); + rows.forEach(row -> { + Map columnAmounts = buildColumnAmounts(context, columns, row); + row.setColumnAmounts(columnAmounts); + }); + return new FmsLedgerMultiColumnRespVO() + .setColumns(convertList(columns, column -> new FmsLedgerMultiColumnRespVO.Column() + .setSubjectId(column.getId()).setSubjectCode(column.getCode()) + .setSubjectName(column.getName()) + .setBalanceDirection(column.getBalanceDirection()))) + .setRows(rows); + } + + // ==================== 核算项目明细账 ==================== + + @Override + public List getAuxiliaryDetailList( + FmsLedgerAuxiliaryListReqVO listReqVO, Long userId) { + // 1. 加载辅助核算账簿上下文 + AuxiliaryLedgerContext context = buildAuxiliaryContext(listReqVO.getAccountSetId(), + listReqVO.getStartMonth(), listReqVO.getEndMonth(), listReqVO.getAuxiliaryTypeId(), + listReqVO.getSubjectId(), userId); + // 2. 校验辅助核算项目 + validateAuxiliaryItem(context, listReqVO.getAuxiliaryItemId()); + + // 3. 计算核算项目明细账 + return buildAuxiliaryDetailRows(context, listReqVO.getAuxiliaryItemId()); + } + + // ==================== 核算项目余额表 ==================== + + @Override + public List getAuxiliaryBalanceList( + FmsLedgerAuxiliaryListReqVO listReqVO, Long userId) { + // 1. 加载辅助核算账簿上下文 + AuxiliaryLedgerContext context = buildAuxiliaryContext(listReqVO.getAccountSetId(), + listReqVO.getStartMonth(), listReqVO.getEndMonth(), listReqVO.getAuxiliaryTypeId(), + listReqVO.getSubjectId(), userId); + // 2. 校验辅助核算项目 + if (listReqVO.getAuxiliaryItemId() != null) { + validateAuxiliaryItem(context, listReqVO.getAuxiliaryItemId()); + } + + // 3. 计算核算项目余额并过滤八项金额全零的项目 + List balances = convertList(context.auxiliaryItems, + item -> buildAuxiliaryBalance(context, item), + item -> (listReqVO.getAuxiliaryItemId() == null + || ObjUtil.equal(item.getId(), listReqVO.getAuxiliaryItemId())) + && (CollUtil.isNotEmpty(filterAuxiliaryEntries(context, item.getId())) + || CollUtil.isNotEmpty(filterInitialAssists(context, item.getId())))); + return filterList(balances, this::hasAuxiliaryBalanceAmount); + } + + // ==================== 数量金额明细账 ==================== + + @Override + public List getQuantityDetailList( + FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 加载账簿上下文并校验科目 + LedgerContext context = buildContext(listReqVO, userId); + FmsSubjectDO subject = context.subjectMap.get(listReqVO.getSubjectId()); + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + + // 2. 仅汇总所选子树中启用数量核算的科目 + Set subjectIds = collectSubjectIds(context, subject.getId()); + Set quantitySubjectIds = convertSet(filterList(context.subjects, + item -> subjectIds.contains(item.getId()) && Boolean.TRUE.equals(item.getQuantityAccounting())), + FmsSubjectDO::getId); + List result = BeanUtils.toBean( + buildLedgerRows(context, subject, true, quantitySubjectIds), FmsLedgerQuantityDetailRespVO.class); + result.forEach(this::fillQuantityDetailUnitPrices); + return result; + } + + /** + * 补充数量明细账的期初、本期、累计和期末单价 + * + * @param row 数量明细账行 + */ + private void fillQuantityDetailUnitPrices(FmsLedgerQuantityDetailRespVO row) { + // 1. 汇总当前行的数量和金额 + BigDecimal periodQuantity = zeroIfNull(row.getDebitQuantity()).add(zeroIfNull(row.getCreditQuantity())); + BigDecimal periodAmount = zeroIfNull(row.getDebitAmount()).add(zeroIfNull(row.getCreditAmount())); + BigDecimal balanceQuantity = zeroIfNull(row.getBalanceQuantity()); + BigDecimal balanceAmount = zeroIfNull(row.getBalance()); + BigDecimal price = row.getUnitPrice(); + // 2. 按行类型计算对应期间单价 + if (row.getRowType().equals(FmsLedgerQuantityDetailRespVO.ROW_TYPE_OPENING)) { + row.setOpeningUnitPrice(unitPrice(balanceAmount, balanceQuantity)); + } else if (row.getRowType().equals(FmsLedgerQuantityDetailRespVO.ROW_TYPE_PERIOD_TOTAL)) { + row.setPeriodUnitPrice(unitPrice(periodAmount, periodQuantity)); + } else if (row.getRowType().equals(FmsLedgerQuantityDetailRespVO.ROW_TYPE_YEAR_TOTAL)) { + row.setYearUnitPrice(unitPrice(periodAmount, periodQuantity)); + } else if (row.getRowType().equals(FmsLedgerQuantityDetailRespVO.ROW_TYPE_VOUCHER)) { + row.setPeriodUnitPrice(price); + } + // 3. 统一计算期末单价 + row.setEndingUnitPrice(unitPrice(balanceAmount, balanceQuantity)); + } + + private BigDecimal unitPrice(BigDecimal amount, BigDecimal quantity) { + return quantity.signum() == 0 ? BigDecimal.ZERO : amount.divide(quantity, 6, RoundingMode.HALF_UP); + } + + // ==================== 数量金额总账 ==================== + + @Override + public List getQuantityGeneralList(FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 加载账簿上下文 + LedgerContext context = buildContext(listReqVO, userId); + + // 2. 计算科目余额;保留有数量业务节点及其非数量父级汇总节点 + List balances = buildSubjectBalanceList(context, listReqVO, + balance -> true, subject -> buildSubjectBalance(context, subject, true)); + pruneQuantityGeneralTree(balances); + return buildQuantityGeneralRespVOList(balances); + } + + /** + * 构建普通账簿计算上下文 + * + * @param listReqVO 列表查询参数 + * @param userId 用户编号 + * @return 普通账簿计算上下文 + */ + private LedgerContext buildContext(FmsLedgerListReqVO listReqVO, Long userId) { + // 1. 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission( + listReqVO.getAccountSetId(), userId); + + // 2. 校验会计期间 + YearMonth startMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth()); + YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth()); + validateLedgerPeriod(accountSet, startMonth, endMonth); + + // 3. 加载财务参数和科目 + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter( + listReqVO.getAccountSetId(), userId); + List subjects = subjectService.getSubjectList( + listReqVO.getAccountSetId(), null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + Map> childMap = new HashMap<>(); + subjects.forEach(subject -> childMap.computeIfAbsent(subject.getParentId(), key -> new ArrayList<>()) + .add(subject.getId())); + + // 4. 加载初始余额和查询期间结束前的凭证分录 + LocalDateTime endTime = endMonth.plusMonths(1).atDay(1).atStartOfDay(); + return new LedgerContext(accountSet, financeParameter, startMonth, endMonth, subjects, subjectMap, childMap, + initialBalanceMapper.selectListByAccountSetId(listReqVO.getAccountSetId()), + ledgerQueryMapper.selectEntryListBeforeTime(listReqVO.getAccountSetId(), endTime)); + } + + /** + * 根据科目范围和级次筛选科目 + * + * @param context 普通账簿计算上下文 + * @param listReqVO 列表查询参数 + * @return 科目列表 + */ + private List filterSubjects(LedgerContext context, FmsLedgerListReqVO listReqVO) { + FmsSubjectDO startSubject = getOptionalSubject(context, listReqVO.getStartSubjectId()); + FmsSubjectDO endSubject = getOptionalSubject(context, listReqVO.getEndSubjectId()); + String startCode = startSubject == null ? null : startSubject.getCode(); + String endCode = endSubject == null ? null : endSubject.getCode(); + Set endSubjectIds = endSubject == null ? Collections.emptySet() + : collectSubjectIds(context, endSubject.getId()); + return filterList(context.subjects, subject -> (listReqVO.getMinLevel() == null + || subject.getLevel() >= listReqVO.getMinLevel()) + && (listReqVO.getMaxLevel() == null || subject.getLevel() <= listReqVO.getMaxLevel()) + && (startCode == null || subject.getCode().compareTo(startCode) >= 0) + && (endCode == null || subject.getCode().compareTo(endCode) <= 0 + || endSubjectIds.contains(subject.getId()))); + } + + /** + * 构建科目余额树 + * + * @param context 普通账簿计算上下文 + * @param listReqVO 列表查询参数 + * @param balancePredicate 余额行保留条件 + * @return 科目余额树 + */ + private List buildSubjectBalanceList(LedgerContext context, + FmsLedgerListReqVO listReqVO, Predicate balancePredicate) { + return buildSubjectBalanceList(context, listReqVO, balancePredicate, + subject -> buildSubjectBalance(context, subject, false)); + } + + /** + * 构建科目余额树 + * + * @param context 普通账簿计算上下文 + * @param listReqVO 列表查询参数 + * @param balancePredicate 余额行保留条件 + * @param balanceBuilder 科目余额构建函数 + * @return 科目余额树 + */ + private List buildSubjectBalanceList(LedgerContext context, + FmsLedgerListReqVO listReqVO, Predicate balancePredicate, + Function balanceBuilder) { + // 1. 计算科目余额并过滤空行 + List subjects = filterSubjects(context, listReqVO); + Map resultMap = convertMap(subjects, FmsSubjectDO::getId, + balanceBuilder, (oldValue, newValue) -> oldValue, + LinkedHashMap::new); + resultMap.values().removeIf(balancePredicate.negate()); + + // 2. 组装科目树 + List roots = new ArrayList<>(); + subjects.forEach(subject -> { + FmsLedgerSubjectBalanceRespVO current = resultMap.get(subject.getId()); + if (current == null) { + return; + } + FmsLedgerSubjectBalanceRespVO parent = resultMap.get(subject.getParentId()); + if (parent == null) { + roots.add(current); + } else { + parent.getChildren().add(current); + } + }); + return roots; + } + + /** + * 获得可选科目,不存在时抛出业务异常 + * + * @param context 普通账簿计算上下文 + * @param subjectId 科目编号 + * @return 科目 + */ + private FmsSubjectDO getOptionalSubject(LedgerContext context, Long subjectId) { + if (subjectId == null) { + return null; + } + FmsSubjectDO subject = context.subjectMap.get(subjectId); + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + return subject; + } + + /** + * 构建指定科目的账簿行 + * + * @param context 普通账簿计算上下文 + * @param subject 科目 + * @param includeVoucherRows 是否包含凭证分录行 + * @return 账簿行列表 + */ + private List buildLedgerRows(LedgerContext context, FmsSubjectDO subject, boolean includeVoucherRows) { + return buildLedgerRows(context, subject, includeVoucherRows, collectSubjectIds(context, subject.getId())); + } + + /** + * 构建指定科目范围的账簿行 + * + * @param context 普通账簿计算上下文 + * @param subject 查询科目 + * @param includeVoucherRows 是否包含凭证分录行 + * @param subjectIds 参与汇总的科目编号集合 + * @return 账簿行列表 + */ + private List buildLedgerRows(LedgerContext context, FmsSubjectDO subject, + boolean includeVoucherRows, Set subjectIds) { + // 1. 筛选科目及下级科目的凭证分录 + List entries = filterList(context.entries, + entry -> subjectIds.contains(entry.getSubjectId())); + + // 2. 计算并添加期初余额行 + BigDecimal openingSignedAmount = calculateInitialSignedAmount(context, subjectIds) + .add(calculateSignedAmount(entries, null, context.startMonth.atDay(1).atStartOfDay())); + BigDecimal openingSignedQuantity = calculateInitialSignedQuantity(context, subjectIds) + .add(calculateSignedQuantity(entries, null, context.startMonth.atDay(1).atStartOfDay())); + List result = new ArrayList<>(); + result.add(buildSummaryRow(context, subject, FmsLedgerDetailRespVO.ROW_TYPE_OPENING, context.startMonth, + context.startMonth.atEndOfMonth(), "期初余额", BigDecimal.ZERO, BigDecimal.ZERO, + BigDecimal.ZERO, BigDecimal.ZERO, openingSignedAmount, openingSignedQuantity)); + + // 3. 逐月计算凭证分录、本期合计和本年累计行 + BigDecimal runningSignedAmount = openingSignedAmount; + BigDecimal runningSignedQuantity = openingSignedQuantity; + YearMonth currentMonth = context.startMonth; + while (!currentMonth.isAfter(context.endMonth)) { + LocalDateTime monthBeginTime = currentMonth.atDay(1).atStartOfDay(); + LocalDateTime nextMonthBeginTime = currentMonth.plusMonths(1).atDay(1).atStartOfDay(); + List monthEntries = entries.stream() + .filter(entry -> !entry.getVoucherTime().isBefore(monthBeginTime) + && entry.getVoucherTime().isBefore(nextMonthBeginTime)) + .collect(Collectors.toList()); + if (includeVoucherRows) { + for (FmsLedgerEntryVO entry : monthEntries) { + runningSignedAmount = runningSignedAmount.add(zeroIfNull(entry.getDebitAmount())) + .subtract(zeroIfNull(entry.getCreditAmount())); + BigDecimal signedQuantity = signedQuantity(entry); + runningSignedQuantity = runningSignedQuantity.add(signedQuantity); + result.add(buildVoucherRow(context, subject, entry, + runningSignedAmount, runningSignedQuantity)); + } + } else { + runningSignedAmount = runningSignedAmount.add(calculateSignedAmount( + monthEntries, null, null)); + runningSignedQuantity = runningSignedQuantity.add(calculateSignedQuantity( + monthEntries, null, null)); + } + BigDecimal periodDebitAmount = sumDebitAmount(monthEntries); + BigDecimal periodCreditAmount = sumCreditAmount(monthEntries); + BigDecimal periodDebitQuantity = sumQuantity(monthEntries, true); + BigDecimal periodCreditQuantity = sumQuantity(monthEntries, false); + result.add(buildSummaryRow(context, subject, FmsLedgerDetailRespVO.ROW_TYPE_PERIOD_TOTAL, + currentMonth, currentMonth.atEndOfMonth(), "本期合计", periodDebitAmount, + periodCreditAmount, periodDebitQuantity, periodCreditQuantity, + runningSignedAmount, runningSignedQuantity)); + + LocalDateTime yearBeginTime = currentMonth.atDay(1).withDayOfYear(1).atStartOfDay(); + List yearEntries = entries.stream() + .filter(entry -> !entry.getVoucherTime().isBefore(yearBeginTime) + && entry.getVoucherTime().isBefore(nextMonthBeginTime)) + .collect(Collectors.toList()); + BigDecimal yearDebitAmount = sumDebitAmount(yearEntries) + .add(calculateInitialYearAmount(context, subjectIds, true, currentMonth.getYear())); + BigDecimal yearCreditAmount = sumCreditAmount(yearEntries) + .add(calculateInitialYearAmount(context, subjectIds, false, currentMonth.getYear())); + BigDecimal yearDebitQuantity = sumQuantity(yearEntries, true) + .add(calculateInitialYearQuantity(context, subjectIds, true, currentMonth.getYear())); + BigDecimal yearCreditQuantity = sumQuantity(yearEntries, false) + .add(calculateInitialYearQuantity(context, subjectIds, false, currentMonth.getYear())); + result.add(buildSummaryRow(context, subject, FmsLedgerDetailRespVO.ROW_TYPE_YEAR_TOTAL, + currentMonth, currentMonth.atEndOfMonth(), "本年累计", yearDebitAmount, + yearCreditAmount, yearDebitQuantity, yearCreditQuantity, + runningSignedAmount, runningSignedQuantity)); + currentMonth = currentMonth.plusMonths(1); + } + return result; + } + + /** + * 构建凭证分录账簿行 + * + * @param context 普通账簿计算上下文 + * @param subject 查询科目 + * @param entry 凭证分录 + * @param signedBalance 余额方向金额 + * @param signedQuantity 余额方向数量 + * @return 凭证分录账簿行 + */ + private FmsLedgerDetailRespVO buildVoucherRow(LedgerContext context, FmsSubjectDO subject, + FmsLedgerEntryVO entry, BigDecimal signedBalance, BigDecimal signedQuantity) { + return fillBalance(context, subject, new FmsLedgerDetailRespVO() + .setRowType(FmsLedgerDetailRespVO.ROW_TYPE_VOUCHER) + .setEntryId(entry.getEntryId()).setEntrySubjectId(entry.getSubjectId()) + .setSubjectId(subject.getId()).setSubjectCode(subject.getCode()) + .setSubjectName(subject.getName()).setPeriod(YearMonth.from(entry.getVoucherTime()).toString()) + .setAccountDate(entry.getVoucherTime().toLocalDate()).setVoucherId(entry.getVoucherId()) + .setVoucherNumber(entry.getVoucherWordName() + "-" + entry.getVoucherNumber()) + .setDigest(entry.getDigest()).setDebitAmount(zeroIfNull(entry.getDebitAmount())) + .setCreditAmount(zeroIfNull(entry.getCreditAmount())) + .setDebitQuantity(isDebit(entry) ? zeroIfNull(entry.getQuantity()) : BigDecimal.ZERO) + .setCreditQuantity(isDebit(entry) ? BigDecimal.ZERO : zeroIfNull(entry.getQuantity())) + .setUnitPrice(zeroIfNull(entry.getUnitPrice())).setQuantityUnit(subject.getQuantityUnit()), + signedBalance, signedQuantity); + } + + /** + * 构建多栏账各科目列金额 + * + * @param context 普通账簿计算上下文 + * @param columns 多栏账科目列 + * @param row 账簿行 + * @return 科目编号与金额 Map + */ + private Map buildColumnAmounts(LedgerContext context, + List columns, FmsLedgerDetailRespVO row) { + Map result = new LinkedHashMap<>(); + for (FmsSubjectDO column : columns) { + BigDecimal amount; + if (row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_VOUCHER)) { + amount = ObjUtil.equal(row.getEntrySubjectId(), column.getId()) + ? nominalAmount(column, row.getDebitAmount(), row.getCreditAmount()) : BigDecimal.ZERO; + } else { + amount = calculateColumnSummaryAmount(context, column, row); + } + result.put(column.getId(), amount); + } + return result; + } + + /** + * 为多栏账每月追加期末余额行 + * + * @param rows 账簿行 + * @return 包含每月期末余额的账簿行 + */ + private List appendMultiColumnEndingRows(List rows) { + List result = new ArrayList<>(); + for (FmsLedgerDetailRespVO row : rows) { + result.add(row); + if (!ObjUtil.equal(row.getRowType(), FmsLedgerDetailRespVO.ROW_TYPE_YEAR_TOTAL)) { + continue; + } + YearMonth period = LocalDateTimeUtils.parseYearMonth(row.getPeriod()); + result.add(BeanUtils.toBean(row, FmsLedgerDetailRespVO.class) + .setRowType(FmsLedgerDetailRespVO.ROW_TYPE_ENDING) + .setAccountDate(period.atEndOfMonth()).setDigest("期末余额") + .setDebitAmount(BigDecimal.ZERO).setCreditAmount(BigDecimal.ZERO) + .setColumnAmounts(null)); + } + return result; + } + + /** + * 计算多栏账科目汇总金额 + * + * @param context 普通账簿计算上下文 + * @param column 多栏账科目列 + * @param row 汇总账簿行 + * @return 科目汇总金额 + */ + private BigDecimal calculateColumnSummaryAmount(LedgerContext context, + FmsSubjectDO column, FmsLedgerDetailRespVO row) { + List entries = filterList(context.entries, + entry -> ObjUtil.equal(entry.getSubjectId(), column.getId())); + YearMonth period = LocalDateTimeUtils.parseYearMonth(row.getPeriod()); + if (row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_OPENING)) { + BigDecimal initialAmount = sumBigDecimal(filterList(context.initialBalances, + initial -> ObjUtil.equal(initial.getSubjectId(), column.getId())), + FmsInitialBalanceDO::getOpeningAmount); + return initialAmount.add(sumBigDecimal(entries, entry -> entry.getVoucherTime() + .isBefore(period.atDay(1).atStartOfDay()) + ? nominalAmount(column, entry.getDebitAmount(), entry.getCreditAmount()) : BigDecimal.ZERO)); + } + LocalDateTime nextMonthBeginTime = period.plusMonths(1).atDay(1).atStartOfDay(); + if (row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_ENDING)) { + BigDecimal initialAmount = sumBigDecimal(filterList(context.initialBalances, + initial -> ObjUtil.equal(initial.getSubjectId(), column.getId())), + FmsInitialBalanceDO::getOpeningAmount); + return initialAmount.add(sumBigDecimal(entries, entry -> entry.getVoucherTime() + .isBefore(nextMonthBeginTime) + ? nominalAmount(column, entry.getDebitAmount(), entry.getCreditAmount()) : BigDecimal.ZERO)); + } + LocalDateTime beginTime = row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_YEAR_TOTAL) + ? LocalDate.of(period.getYear(), 1, 1).atStartOfDay() : period.atDay(1).atStartOfDay(); + BigDecimal amount = sumBigDecimal(filterEntriesByTime(entries, beginTime, nextMonthBeginTime), + entry -> nominalAmount(column, entry.getDebitAmount(), entry.getCreditAmount())); + if (row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_YEAR_TOTAL) + && context.accountSet.getStartTime().getYear() == period.getYear()) { + amount = amount.add(sumBigDecimal(filterList(context.initialBalances, + initial -> ObjUtil.equal(initial.getSubjectId(), column.getId())), + initial -> ObjUtil.equal(column.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()) + ? zeroIfNull(initial.getYearDebitAmount()).subtract( + zeroIfNull(initial.getYearCreditAmount())) + : zeroIfNull(initial.getYearCreditAmount()).subtract( + zeroIfNull(initial.getYearDebitAmount())))); + } + return amount; + } + + /** + * 按科目余额方向计算名义金额 + * + * @param subject 科目 + * @param debitAmount 借方金额 + * @param creditAmount 贷方金额 + * @return 名义金额 + */ + private BigDecimal nominalAmount(FmsSubjectDO subject, BigDecimal debitAmount, + BigDecimal creditAmount) { + return ObjUtil.equal(subject.getBalanceDirection(), FmsDebitCreditDirectionEnum.DEBIT.getType()) + ? zeroIfNull(debitAmount).subtract(zeroIfNull(creditAmount)) + : zeroIfNull(creditAmount).subtract(zeroIfNull(debitAmount)); + } + + /** + * 构建账簿汇总行 + * + * @param context 普通账簿计算上下文 + * @param subject 科目 + * @param rowType 行类型 + * @param period 会计期间 + * @param accountDate 记账日期 + * @param digest 摘要 + * @param debitAmount 借方金额 + * @param creditAmount 贷方金额 + * @param debitQuantity 借方数量 + * @param creditQuantity 贷方数量 + * @param signedBalance 余额方向金额 + * @param signedQuantity 余额方向数量 + * @return 账簿汇总行 + */ + private FmsLedgerDetailRespVO buildSummaryRow(LedgerContext context, FmsSubjectDO subject, + Integer rowType, YearMonth period, LocalDate accountDate, String digest, + BigDecimal debitAmount, BigDecimal creditAmount, BigDecimal debitQuantity, + BigDecimal creditQuantity, BigDecimal signedBalance, BigDecimal signedQuantity) { + return fillBalance(context, subject, new FmsLedgerDetailRespVO().setRowType(rowType) + .setSubjectId(subject.getId()).setSubjectCode(subject.getCode()) + .setSubjectName(subject.getName()).setPeriod(period.toString()).setAccountDate(accountDate) + .setDigest(digest).setDebitAmount(debitAmount).setCreditAmount(creditAmount) + .setDebitQuantity(debitQuantity).setCreditQuantity(creditQuantity) + .setQuantityUnit(subject.getQuantityUnit()), signedBalance, signedQuantity); + } + + /** + * 按账簿余额方向模式填充余额 + * + * @param context 普通账簿计算上下文 + * @param subject 科目 + * @param result 账簿行 + * @param signedBalance 余额方向金额 + * @param signedQuantity 余额方向数量 + * @return 账簿行 + */ + private FmsLedgerDetailRespVO fillBalance(LedgerContext context, FmsSubjectDO subject, FmsLedgerDetailRespVO result, + BigDecimal signedBalance, BigDecimal signedQuantity) { + Integer balanceMode = context.financeParameter.getLedgerBalanceMode(); + if (ObjUtil.equal(balanceMode, FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode())) { + boolean debitSubject = ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()); + result.setBalanceDirection(signedBalance.signum() == 0 ? FmsBalanceDirectionEnum.FLAT.getName() + : FmsDebitCreditDirectionEnum.valueOf(subject.getBalanceDirection()).getName()) + .setBalance(debitSubject ? signedBalance : signedBalance.negate()) + .setBalanceQuantity(debitSubject ? signedQuantity : signedQuantity.negate()); + } else { + result.setBalanceDirection(balanceDirection(signedBalance)) + .setBalance(signedBalance.abs()).setBalanceQuantity(signedQuantity.abs()); + } + return result; + } + + /** + * 构建科目余额 + * + * @param context 普通账簿计算上下文 + * @param subject 科目 + * @return 科目余额 + */ + private FmsLedgerSubjectBalanceRespVO buildSubjectBalance( + LedgerContext context, FmsSubjectDO subject, boolean quantityGeneral) { + // 1. 筛选科目及下级科目的凭证分录 + Set subjectIds = collectSubjectIds(context, subject.getId()); + List entries = filterList(context.entries, + entry -> subjectIds.contains(entry.getSubjectId())); + + // 2. 计算期初余额和查询期间发生额 + LocalDateTime startTime = context.startMonth.atDay(1).atStartOfDay(); + LocalDateTime endTime = context.endMonth.plusMonths(1).atDay(1).atStartOfDay(); + BigDecimal openingSignedAmount = calculateInitialSignedAmount(context, subjectIds) + .add(calculateSignedAmount(entries, null, startTime)); + BigDecimal openingSignedQuantity = calculateInitialSignedQuantity(context, subjectIds) + .add(calculateSignedQuantity(entries, null, startTime)); + List periodEntries = filterEntriesByTime(entries, startTime, endTime); + LocalDateTime yearBeginTime = LocalDate.of(context.endMonth.getYear(), 1, 1).atStartOfDay(); + List yearEntries = filterEntriesByTime(entries, yearBeginTime, endTime); + BigDecimal endingSignedAmount = openingSignedAmount.add(calculateSignedAmount( + periodEntries, null, null)); + BigDecimal endingSignedQuantity = openingSignedQuantity.add(calculateSignedQuantity( + periodEntries, null, null)); + BigDecimal yearDebitQuantity = sumQuantity(yearEntries, true).add(calculateInitialYearQuantity( + context, subjectIds, true, context.endMonth.getYear())); + BigDecimal yearCreditQuantity = sumQuantity(yearEntries, false).add(calculateInitialYearQuantity( + context, subjectIds, false, context.endMonth.getYear())); + + // 3. 组装科目余额响应 + FmsLedgerSubjectBalanceRespVO result = new FmsLedgerSubjectBalanceRespVO() + .setNodeKey("S:" + subject.getId()) + .setNodeType(FmsLedgerSubjectBalanceRespVO.NODE_TYPE_SUBJECT) + .setSubjectId(subject.getId()) + .setSubjectCode(subject.getCode()).setSubjectName(subject.getName()) + .setLevel(subject.getLevel()).setQuantityAccounting(subject.getQuantityAccounting()) + .setQuantityUnit(subject.getQuantityUnit()) + .setOpeningDebitAmount(debitPart(openingSignedAmount)) + .setOpeningCreditAmount(creditPart(openingSignedAmount)) + .setOpeningBalanceDirection(balanceDirection(openingSignedAmount)) + .setOpeningQuantity(openingSignedQuantity.abs()) + .setOpeningUnitPrice(calculateUnitPrice(openingSignedAmount, openingSignedQuantity)) + .setPeriodDebitAmount(sumDebitAmount(periodEntries)) + .setPeriodCreditAmount(sumCreditAmount(periodEntries)) + .setPeriodDebitQuantity(sumQuantity(periodEntries, true)) + .setPeriodCreditQuantity(sumQuantity(periodEntries, false)) + .setYearDebitAmount(sumDebitAmount(yearEntries).add(calculateInitialYearAmount( + context, subjectIds, true, context.endMonth.getYear()))) + .setYearCreditAmount(sumCreditAmount(yearEntries).add(calculateInitialYearAmount( + context, subjectIds, false, context.endMonth.getYear()))) + .setYearDebitQuantity(yearDebitQuantity).setYearCreditQuantity(yearCreditQuantity) + .setEndingDebitAmount(debitPart(endingSignedAmount)) + .setEndingCreditAmount(creditPart(endingSignedAmount)) + .setEndingBalanceDirection(balanceDirection(endingSignedAmount)) + .setEndingQuantity(endingSignedQuantity.abs()) + .setEndingUnitPrice(calculateUnitPrice(endingSignedAmount, endingSignedQuantity)); + if (quantityGeneral && ObjUtil.equal(context.financeParameter.getLedgerBalanceMode(), + FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode())) { + fillQuantityGeneralBalance(subject, result, openingSignedAmount, openingSignedQuantity, + endingSignedAmount, endingSignedQuantity); + } + return result; + } + + /** + * 构建科目对应的辅助核算组合余额 Map + * + * @param context 普通账簿计算上下文 + * @param auxiliaryItemMap 辅助核算项目 Map + * @return 科目编号与辅助核算组合余额 Map + */ + private Map> buildAuxiliaryCombinationBalanceMap( + LedgerContext context, Map auxiliaryItemMap) { + // 1. 收集期初余额和凭证分录中实际使用的辅助核算组合 + Map combinationMap = new LinkedHashMap<>(); + for (FmsInitialBalanceDO initial : context.initialBalances) { + for (FmsInitialBalanceDO.AssistBalance assist : CollUtil.emptyIfNull(initial.getAssistBalances())) { + if (assist.getAssistCombinationId() == null) { + continue; + } + AuxiliaryCombinationRecord record = combinationMap.computeIfAbsent( + assist.getAssistCombinationId(), id -> new AuxiliaryCombinationRecord( + id, initial.getSubjectId(), buildAuxiliarySnapshots(assist.getAuxiliaries()), + new ArrayList<>())); + record.initialAssists.add(assist); + } + } + for (FmsLedgerEntryVO entry : context.entries) { + if (entry.getAssistCombinationId() == null || CollUtil.isEmpty(entry.getAuxiliaries())) { + continue; + } + combinationMap.computeIfAbsent(entry.getAssistCombinationId(), id -> new AuxiliaryCombinationRecord( + id, entry.getSubjectId(), buildAuxiliarySnapshotsFromEntries(entry.getAuxiliaries()), + new ArrayList<>())); + } + + // 2. 计算非零组合余额并按科目分组 + Map> result = new LinkedHashMap<>(); + for (AuxiliaryCombinationRecord record : combinationMap.values()) { + FmsSubjectDO subject = context.subjectMap.get(record.subjectId); + if (subject == null) { + continue; + } + FmsLedgerSubjectBalanceRespVO balance = buildAuxiliaryCombinationBalance( + context, subject, record, auxiliaryItemMap); + if (hasSubjectBalanceAmount(balance)) { + result.computeIfAbsent(record.subjectId, key -> new ArrayList<>()).add(balance); + } + } + return result; + } + + /** + * 构建辅助核算组合余额 + * + * @param context 普通账簿计算上下文 + * @param subject 科目 + * @param record 辅助核算组合记录 + * @param auxiliaryItemMap 辅助核算项目 Map + * @return 辅助核算组合余额 + */ + private FmsLedgerSubjectBalanceRespVO buildAuxiliaryCombinationBalance(LedgerContext context, + FmsSubjectDO subject, AuxiliaryCombinationRecord record, + Map auxiliaryItemMap) { + // 1. 筛选组合分录并计算期初、期末余额 + List entries = filterList(context.entries, + entry -> ObjUtil.equal(entry.getSubjectId(), subject.getId()) + && ObjUtil.equal(entry.getAssistCombinationId(), record.id)); + LocalDateTime startTime = context.startMonth.atDay(1).atStartOfDay(); + LocalDateTime endTime = context.endMonth.plusMonths(1).atDay(1).atStartOfDay(); + boolean debitSubject = ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()); + BigDecimal initialAmount = sumBigDecimal(record.initialAssists, + assist -> zeroIfNull(assist.getOpeningAmount())); + BigDecimal initialQuantity = sumBigDecimal(record.initialAssists, + assist -> zeroIfNull(assist.getOpeningQuantity())); + BigDecimal openingSignedAmount = (debitSubject ? initialAmount : initialAmount.negate()) + .add(calculateSignedAmount(entries, null, startTime)); + BigDecimal openingSignedQuantity = (debitSubject ? initialQuantity : initialQuantity.negate()) + .add(calculateSignedQuantity(entries, null, startTime)); + List periodEntries = filterEntriesByTime(entries, startTime, endTime); + BigDecimal endingSignedAmount = openingSignedAmount.add(calculateSignedAmount( + periodEntries, null, null)); + BigDecimal endingSignedQuantity = openingSignedQuantity.add(calculateSignedQuantity( + periodEntries, null, null)); + + // 2. 计算本年累计发生额 + LocalDateTime yearBeginTime = LocalDate.of(context.endMonth.getYear(), 1, 1).atStartOfDay(); + List yearEntries = filterEntriesByTime(entries, yearBeginTime, endTime); + BigDecimal initialYearDebitAmount = BigDecimal.ZERO; + BigDecimal initialYearCreditAmount = BigDecimal.ZERO; + BigDecimal initialYearDebitQuantity = BigDecimal.ZERO; + BigDecimal initialYearCreditQuantity = BigDecimal.ZERO; + if (context.accountSet.getStartTime().getYear() == context.endMonth.getYear()) { + initialYearDebitAmount = sumBigDecimal(record.initialAssists, + assist -> zeroIfNull(assist.getYearDebitAmount())); + initialYearCreditAmount = sumBigDecimal(record.initialAssists, + assist -> zeroIfNull(assist.getYearCreditAmount())); + initialYearDebitQuantity = sumBigDecimal(record.initialAssists, + assist -> zeroIfNull(assist.getYearDebitQuantity())); + initialYearCreditQuantity = sumBigDecimal(record.initialAssists, + assist -> zeroIfNull(assist.getYearCreditQuantity())); + } + + // 3. 组装辅助核算组合节点 + String codeSuffix = record.items.stream().map(item -> { + FmsAuxiliaryItemDO auxiliaryItem = auxiliaryItemMap.get(item.itemId); + return auxiliaryItem == null ? String.valueOf(item.itemId) : auxiliaryItem.getCode(); + }).collect(Collectors.joining("_")); + String nameSuffix = record.items.stream().map(item -> { + FmsAuxiliaryItemDO auxiliaryItem = auxiliaryItemMap.get(item.itemId); + return item.name != null ? item.name + : auxiliaryItem == null ? String.valueOf(item.itemId) : auxiliaryItem.getName(); + }).collect(Collectors.joining("_")); + return new FmsLedgerSubjectBalanceRespVO().setNodeKey("A:" + record.id) + .setNodeType(FmsLedgerSubjectBalanceRespVO.NODE_TYPE_AUXILIARY_COMBINATION) + .setSubjectId(subject.getId()).setAssistCombinationId(record.id) + .setSubjectCode(subject.getCode() + "_" + codeSuffix) + .setSubjectName(subject.getName() + "_" + nameSuffix).setLevel(subject.getLevel() + 1) + .setQuantityAccounting(subject.getQuantityAccounting()).setQuantityUnit(subject.getQuantityUnit()) + .setOpeningDebitAmount(debitPart(openingSignedAmount)) + .setOpeningCreditAmount(creditPart(openingSignedAmount)) + .setOpeningBalanceDirection(balanceDirection(openingSignedAmount)) + .setOpeningQuantity(openingSignedQuantity.abs()) + .setOpeningUnitPrice(calculateUnitPrice(openingSignedAmount, openingSignedQuantity)) + .setPeriodDebitAmount(sumDebitAmount(periodEntries)) + .setPeriodCreditAmount(sumCreditAmount(periodEntries)) + .setPeriodDebitQuantity(sumQuantity(periodEntries, true)) + .setPeriodCreditQuantity(sumQuantity(periodEntries, false)) + .setYearDebitAmount(sumDebitAmount(yearEntries).add(initialYearDebitAmount)) + .setYearCreditAmount(sumCreditAmount(yearEntries).add(initialYearCreditAmount)) + .setYearDebitQuantity(sumQuantity(yearEntries, true).add(initialYearDebitQuantity)) + .setYearCreditQuantity(sumQuantity(yearEntries, false).add(initialYearCreditQuantity)) + .setEndingDebitAmount(debitPart(endingSignedAmount)) + .setEndingCreditAmount(creditPart(endingSignedAmount)) + .setEndingBalanceDirection(balanceDirection(endingSignedAmount)) + .setEndingQuantity(endingSignedQuantity.abs()) + .setEndingUnitPrice(calculateUnitPrice(endingSignedAmount, endingSignedQuantity)); + } + + /** + * 将辅助核算组合余额挂到对应科目节点 + * + * @param balances 科目余额树 + * @param auxiliaryBalanceMap 科目编号与辅助核算组合余额 Map + */ + private void appendAuxiliaryCombinationBalances(List balances, + Map> auxiliaryBalanceMap) { + for (FmsLedgerSubjectBalanceRespVO balance : balances) { + appendAuxiliaryCombinationBalances(balance.getChildren(), auxiliaryBalanceMap); + balance.getChildren().addAll(auxiliaryBalanceMap.getOrDefault( + balance.getSubjectId(), Collections.emptyList())); + } + } + + private List buildAuxiliarySnapshots( + List auxiliaries) { + return convertList(CollUtil.emptyIfNull(auxiliaries), + item -> new AuxiliarySnapshot(item.getItemId(), item.getName())); + } + + private List buildAuxiliarySnapshotsFromEntries( + List auxiliaries) { + return convertList(CollUtil.emptyIfNull(auxiliaries), + item -> new AuxiliarySnapshot(item.getItemId(), item.getName())); + } + + /** + * 按科目方向填充数量金额总账余额 + * + * @param subject 科目 + * @param result 科目余额 + * @param openingSignedAmount 期初借贷净额 + * @param openingSignedQuantity 期初借贷净数量 + * @param endingSignedAmount 期末借贷净额 + * @param endingSignedQuantity 期末借贷净数量 + */ + private void fillQuantityGeneralBalance(FmsSubjectDO subject, FmsLedgerSubjectBalanceRespVO result, + BigDecimal openingSignedAmount, BigDecimal openingSignedQuantity, + BigDecimal endingSignedAmount, BigDecimal endingSignedQuantity) { + boolean debitSubject = ObjUtil.equal(subject.getBalanceDirection(), FmsDebitCreditDirectionEnum.DEBIT.getType()); + String subjectDirection = FmsDebitCreditDirectionEnum.valueOf(subject.getBalanceDirection()).getName(); + BigDecimal openingBalance = debitSubject ? openingSignedAmount : openingSignedAmount.negate(); + BigDecimal endingBalance = debitSubject ? endingSignedAmount : endingSignedAmount.negate(); + result.setOpeningBalanceDirection(openingBalance.signum() == 0 ? FmsBalanceDirectionEnum.FLAT.getName() : subjectDirection) + .setOpeningDebitAmount(debitSubject ? openingBalance : BigDecimal.ZERO) + .setOpeningCreditAmount(debitSubject ? BigDecimal.ZERO : openingBalance) + .setOpeningQuantity(debitSubject ? openingSignedQuantity : openingSignedQuantity.negate()) + .setEndingBalanceDirection(endingBalance.signum() == 0 ? FmsBalanceDirectionEnum.FLAT.getName() : subjectDirection) + .setEndingDebitAmount(debitSubject ? endingBalance : BigDecimal.ZERO) + .setEndingCreditAmount(debitSubject ? BigDecimal.ZERO : endingBalance) + .setEndingQuantity(debitSubject ? endingSignedQuantity : endingSignedQuantity.negate()); + } + + /** + * 判断科目余额表是否存在非零金额 + * + * @param balance 科目余额 + * @return 是否存在非零金额 + */ + private boolean hasSubjectBalanceAmount(FmsLedgerSubjectBalanceRespVO balance) { + return hasAnyNonZero(balance.getOpeningDebitAmount(), balance.getOpeningCreditAmount(), + balance.getPeriodDebitAmount(), balance.getPeriodCreditAmount(), + balance.getYearDebitAmount(), balance.getYearCreditAmount(), + balance.getEndingDebitAmount(), balance.getEndingCreditAmount()); + } + + /** + * 判断数量金额总账期初或本期是否存在非零金额、数量 + * + * @param balance 科目余额 + * @return 期初或本期是否存在非零金额、数量 + */ + private boolean hasQuantityGeneralBalance(FmsLedgerSubjectBalanceRespVO balance) { + return hasAnyNonZero(balance.getOpeningDebitAmount(), balance.getOpeningCreditAmount(), + balance.getPeriodDebitAmount(), balance.getPeriodCreditAmount(), balance.getOpeningQuantity(), + balance.getPeriodDebitQuantity(), balance.getPeriodCreditQuantity()); + } + + /** + * 裁剪数量金额总账树:保留数量核算节点及其必要的父级汇总节点。 + * + * @param balances 科目余额树 + */ + private void pruneQuantityGeneralTree(List balances) { + for (int index = balances.size() - 1; index >= 0; index--) { + FmsLedgerSubjectBalanceRespVO balance = balances.get(index); + pruneQuantityGeneralTree(balance.getChildren()); + if (CollUtil.isEmpty(balance.getChildren()) && !hasQuantityGeneralBalance(balance)) { + balances.remove(index); + } + } + } + + /** + * 判断核算项目余额表是否存在非零金额 + * + * @param balance 核算项目余额 + * @return 是否存在非零金额 + */ + private boolean hasAuxiliaryBalanceAmount(FmsLedgerAuxiliaryBalanceRespVO balance) { + return hasAnyNonZero(balance.getOpeningDebitAmount(), balance.getOpeningCreditAmount(), + balance.getPeriodDebitAmount(), balance.getPeriodCreditAmount(), + balance.getYearDebitAmount(), balance.getYearCreditAmount(), + balance.getEndingDebitAmount(), balance.getEndingCreditAmount()); + } + + /** + * 判断数值数组中是否存在非零值 + * + * @param values 数值数组 + * @return 是否存在非零值 + */ + private boolean hasAnyNonZero(BigDecimal... values) { + for (BigDecimal value : values) { + if (zeroIfNull(value).signum() != 0) { + return true; + } + } + return false; + } + + /** + * 收集科目及其全部下级科目编号 + * + * @param context 普通账簿计算上下文 + * @param subjectId 科目编号 + * @return 科目编号集合 + */ + private Set collectSubjectIds(LedgerContext context, Long subjectId) { + Set result = new LinkedHashSet<>(); + collectSubjectIds(context.childMap, subjectId, result); + return result; + } + + /** + * 递归收集科目及其全部下级科目编号 + * + * @param childMap 父科目编号与子科目编号 Map + * @param subjectId 科目编号 + * @param result 科目编号集合 + */ + private void collectSubjectIds(Map> childMap, Long subjectId, Set result) { + result.add(subjectId); + for (Long childId : childMap.getOrDefault(subjectId, Collections.emptyList())) { + collectSubjectIds(childMap, childId, result); + } + } + + /** + * 计算科目初始余额方向金额 + * + * @param context 普通账簿计算上下文 + * @param subjectIds 科目编号集合 + * @return 初始余额方向金额 + */ + private BigDecimal calculateInitialSignedAmount(LedgerContext context, Set subjectIds) { + BigDecimal result = BigDecimal.ZERO; + for (FmsInitialBalanceDO initial : context.initialBalances) { + if (subjectIds.contains(initial.getSubjectId())) { + FmsSubjectDO subject = context.subjectMap.get(initial.getSubjectId()); + result = result.add(ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()) + ? zeroIfNull(initial.getOpeningAmount()) : zeroIfNull(initial.getOpeningAmount()).negate()); + } + } + return result; + } + + /** + * 计算科目初始余额方向数量 + * + * @param context 普通账簿计算上下文 + * @param subjectIds 科目编号集合 + * @return 初始余额方向数量 + */ + private BigDecimal calculateInitialSignedQuantity(LedgerContext context, Set subjectIds) { + BigDecimal result = BigDecimal.ZERO; + for (FmsInitialBalanceDO initial : context.initialBalances) { + if (subjectIds.contains(initial.getSubjectId())) { + FmsSubjectDO subject = context.subjectMap.get(initial.getSubjectId()); + result = result.add(ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()) + ? zeroIfNull(initial.getOpeningQuantity()) + : zeroIfNull(initial.getOpeningQuantity()).negate()); + } + } + return result; + } + + /** + * 计算账套启用年度的年初累计发生额 + * + * @param context 普通账簿计算上下文 + * @param subjectIds 科目编号集合 + * @param debit 是否借方 + * @param year 年度 + * @return 年初累计发生额 + */ + private BigDecimal calculateInitialYearAmount(LedgerContext context, Set subjectIds, boolean debit, int year) { + if (context.accountSet.getStartTime().getYear() != year) { + return BigDecimal.ZERO; + } + return sumBigDecimal(filterList(context.initialBalances, + initial -> subjectIds.contains(initial.getSubjectId())), + initial -> debit ? initial.getYearDebitAmount() : initial.getYearCreditAmount()); + } + + /** + * 计算账套启用年度的年初累计数量 + * + * @param context 普通账簿计算上下文 + * @param subjectIds 科目编号集合 + * @param debit 是否借方 + * @param year 年度 + * @return 年初累计数量 + */ + private BigDecimal calculateInitialYearQuantity(LedgerContext context, Set subjectIds, boolean debit, int year) { + if (context.accountSet.getStartTime().getYear() != year) { + return BigDecimal.ZERO; + } + return sumBigDecimal(filterList(context.initialBalances, + initial -> subjectIds.contains(initial.getSubjectId())), + initial -> debit ? initial.getYearDebitQuantity() : initial.getYearCreditQuantity()); + } + + /** + * 计算指定时间范围的借贷方向净额 + * + * @param entries 凭证分录列表 + * @param beginTime 开始时间,为空时不限制 + * @param endTime 结束时间,为空时不限制 + * @return 借贷方向净额 + */ + private BigDecimal calculateSignedAmount(List entries, + LocalDateTime beginTime, LocalDateTime endTime) { + return sumBigDecimal(filterEntriesByTime(entries, beginTime, endTime), + entry -> zeroIfNull(entry.getDebitAmount()).subtract(zeroIfNull(entry.getCreditAmount()))); + } + + /** + * 计算指定时间范围的借贷方向净数量 + * + * @param entries 凭证分录列表 + * @param beginTime 开始时间,为空时不限制 + * @param endTime 结束时间,为空时不限制 + * @return 借贷方向净数量 + */ + private BigDecimal calculateSignedQuantity(List entries, + LocalDateTime beginTime, LocalDateTime endTime) { + return sumBigDecimal(filterEntriesByTime(entries, beginTime, endTime), this::signedQuantity); + } + + /** + * 计算凭证分录的借贷方向数量 + * + * @param entry 凭证分录 + * @return 借贷方向数量 + */ + private BigDecimal signedQuantity(FmsLedgerEntryVO entry) { + return isDebit(entry) ? zeroIfNull(entry.getQuantity()) : zeroIfNull(entry.getQuantity()).negate(); + } + + /** + * 汇总借方金额 + * + * @param entries 凭证分录列表 + * @return 借方金额 + */ + private BigDecimal sumDebitAmount(List entries) { + return sumBigDecimal(entries, FmsLedgerEntryVO::getDebitAmount); + } + + /** + * 汇总贷方金额 + * + * @param entries 凭证分录列表 + * @return 贷方金额 + */ + private BigDecimal sumCreditAmount(List entries) { + return sumBigDecimal(entries, FmsLedgerEntryVO::getCreditAmount); + } + + /** + * 汇总指定借贷方向的数量 + * + * @param entries 凭证分录列表 + * @param debit 是否借方 + * @return 数量 + */ + private BigDecimal sumQuantity(List entries, boolean debit) { + return sumBigDecimal(filterList(entries, entry -> debit == isDebit(entry)), + FmsLedgerEntryVO::getQuantity); + } + + /** + * 判断凭证分录是否为借方分录 + * + * @param entry 凭证分录 + * @return 是否为借方分录 + */ + private boolean isDebit(FmsLedgerEntryVO entry) { + return zeroIfNull(entry.getDebitAmount()).signum() != 0; + } + + /** + * 提取借方余额 + * + * @param signedAmount 余额方向金额 + * @return 借方余额 + */ + private BigDecimal debitPart(BigDecimal signedAmount) { + return signedAmount.signum() > 0 ? signedAmount : BigDecimal.ZERO; + } + + /** + * 提取贷方余额 + * + * @param signedAmount 余额方向金额 + * @return 贷方余额 + */ + private BigDecimal creditPart(BigDecimal signedAmount) { + return signedAmount.signum() < 0 ? signedAmount.abs() : BigDecimal.ZERO; + } + + /** + * 获得余额方向 + * + * @param signedAmount 余额方向金额 + * @return 余额方向 + */ + private String balanceDirection(BigDecimal signedAmount) { + return FmsBalanceDirectionEnum.valueOf(signedAmount).getName(); + } + + /** + * 根据结存金额和数量计算单价 + * + * @param signedAmount 余额方向金额 + * @param signedQuantity 余额方向数量 + * @return 单价 + */ + private BigDecimal calculateUnitPrice(BigDecimal signedAmount, BigDecimal signedQuantity) { + if (signedQuantity.signum() == 0) { + return BigDecimal.ZERO; + } + return signedAmount.abs().divide(signedQuantity.abs(), 6, RoundingMode.HALF_UP); + } + + /** + * 构建辅助核算账簿计算上下文 + * + * @param accountSetId 账套编号 + * @param startMonthValue 开始会计期间 + * @param endMonthValue 结束会计期间 + * @param auxiliaryTypeId 辅助核算类别编号 + * @param subjectId 科目编号 + * @param userId 用户编号 + * @return 辅助核算账簿计算上下文 + */ + private AuxiliaryLedgerContext buildAuxiliaryContext(Long accountSetId, String startMonthValue, + String endMonthValue, Long auxiliaryTypeId, Long subjectId, Long userId) { + // 1. 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 校验会计期间 + YearMonth startMonth = LocalDateTimeUtils.parseYearMonth(startMonthValue); + YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(endMonthValue); + validateLedgerPeriod(accountSet, startMonth, endMonth); + + // 3. 加载财务参数、辅助核算项目和科目 + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(accountSetId, userId); + List auxiliaryItems = auxiliaryItemService.getAuxiliaryItemList( + accountSetId, auxiliaryTypeId, userId); + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + Map> childMap = new HashMap<>(); + subjects.forEach(subject -> childMap.computeIfAbsent(subject.getParentId(), key -> new ArrayList<>()) + .add(subject.getId())); + + // 4. 确定科目查询范围 + Set subjectIds; + if (subjectId == null) { + subjectIds = subjects.stream().map(FmsSubjectDO::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } else { + if (!subjectMap.containsKey(subjectId)) { + throw exception(SUBJECT_NOT_EXISTS); + } + subjectIds = collectSubjectIds(childMap, subjectId); + } + + // 5. 加载初始余额和查询期间结束前的凭证分录 + List initialBalances = initialBalanceMapper.selectListByAccountSetId(accountSetId); + List entries = ledgerQueryMapper.selectEntryListBeforeTime( + accountSetId, endMonth.plusMonths(1).atDay(1).atStartOfDay()); + return new AuxiliaryLedgerContext(accountSet, financeParameter, startMonth, endMonth, + auxiliaryTypeId, subjectId, auxiliaryItems, subjectMap, subjectIds, initialBalances, entries); + } + + /** + * 校验账簿查询会计期间 + * + * @param accountSet 账套 + * @param startMonth 开始会计期间 + * @param endMonth 结束会计期间 + */ + private void validateLedgerPeriod(FmsAccountSetDO accountSet, YearMonth startMonth, YearMonth endMonth) { + if (startMonth.isAfter(endMonth)) { + throw exception(LEDGER_PERIOD_INVALID); + } + if (startMonth.isBefore(YearMonth.from(accountSet.getStartTime()))) { + throw exception(LEDGER_PERIOD_BEFORE_ACCOUNT_START); + } + } + + /** + * 构建核算项目明细账行 + * + * @param context 辅助核算账簿计算上下文 + * @param auxiliaryItemId 辅助核算项目编号 + * @return 核算项目明细账行列表 + */ + private List buildAuxiliaryDetailRows(AuxiliaryLedgerContext context, Long auxiliaryItemId) { + // 1. 筛选核算项目凭证分录并计算期初余额 + List entries = filterAuxiliaryEntries(context, auxiliaryItemId); + LocalDateTime startTime = context.startMonth.atDay(1).atStartOfDay(); + BigDecimal runningSignedAmount = calculateAuxiliaryInitialSignedAmount(context, auxiliaryItemId) + .add(calculateSignedAmount(entries, null, startTime)); + + // 2. 添加期初余额行 + List result = new ArrayList<>(); + result.add(buildAuxiliarySummaryRow(context, FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_OPENING, + context.startMonth, "期初余额", BigDecimal.ZERO, BigDecimal.ZERO, runningSignedAmount)); + + // 3. 逐月计算凭证分录、本期合计和本年累计行 + YearMonth currentMonth = context.startMonth; + while (!currentMonth.isAfter(context.endMonth)) { + LocalDateTime monthBeginTime = currentMonth.atDay(1).atStartOfDay(); + LocalDateTime nextMonthBeginTime = currentMonth.plusMonths(1).atDay(1).atStartOfDay(); + List monthEntries = filterEntriesByTime( + entries, monthBeginTime, nextMonthBeginTime); + for (FmsLedgerEntryVO entry : monthEntries) { + runningSignedAmount = runningSignedAmount.add(zeroIfNull(entry.getDebitAmount())) + .subtract(zeroIfNull(entry.getCreditAmount())); + result.add(buildAuxiliaryVoucherRow(context, entry, runningSignedAmount)); + } + result.add(buildAuxiliarySummaryRow(context, + FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_PERIOD_TOTAL, currentMonth, "本期合计", + sumDebitAmount(monthEntries), sumCreditAmount(monthEntries), runningSignedAmount)); + + LocalDateTime yearBeginTime = LocalDate.of(currentMonth.getYear(), 1, 1).atStartOfDay(); + List yearEntries = filterEntriesByTime( + entries, yearBeginTime, nextMonthBeginTime); + result.add(buildAuxiliarySummaryRow(context, + FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_YEAR_TOTAL, currentMonth, "本年累计", + sumDebitAmount(yearEntries).add(calculateAuxiliaryInitialYearAmount( + context, auxiliaryItemId, true, currentMonth.getYear())), + sumCreditAmount(yearEntries).add(calculateAuxiliaryInitialYearAmount( + context, auxiliaryItemId, false, currentMonth.getYear())), + runningSignedAmount)); + currentMonth = currentMonth.plusMonths(1); + } + return result; + } + + /** + * 构建核算项目凭证分录行 + * + * @param context 辅助核算账簿计算上下文 + * @param entry 凭证分录 + * @param signedBalance 余额方向金额 + * @return 核算项目凭证分录行 + */ + private FmsLedgerAuxiliaryDetailRespVO buildAuxiliaryVoucherRow(AuxiliaryLedgerContext context, + FmsLedgerEntryVO entry, BigDecimal signedBalance) { + FmsSubjectDO subject = context.subjectMap.get(entry.getSubjectId()); + return fillAuxiliaryBalance(context, new FmsLedgerAuxiliaryDetailRespVO() + .setRowType(FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_VOUCHER) + .setEntryId(entry.getEntryId()).setSubjectId(entry.getSubjectId()) + .setSubjectCode(subject.getCode()).setSubjectName(subject.getName()) + .setPeriod(YearMonth.from(entry.getVoucherTime()).toString()) + .setAccountDate(entry.getVoucherTime().toLocalDate()).setVoucherId(entry.getVoucherId()) + .setVoucherNumber(entry.getVoucherWordName() + "-" + entry.getVoucherNumber()) + .setDigest(entry.getDigest()).setDebitAmount(zeroIfNull(entry.getDebitAmount())) + .setCreditAmount(zeroIfNull(entry.getCreditAmount())), signedBalance); + } + + /** + * 构建核算项目明细账汇总行 + * + * @param context 辅助核算账簿计算上下文 + * @param rowType 行类型 + * @param period 会计期间 + * @param digest 摘要 + * @param debitAmount 借方金额 + * @param creditAmount 贷方金额 + * @param signedBalance 余额方向金额 + * @return 核算项目明细账汇总行 + */ + private FmsLedgerAuxiliaryDetailRespVO buildAuxiliarySummaryRow(AuxiliaryLedgerContext context, + Integer rowType, YearMonth period, String digest, BigDecimal debitAmount, + BigDecimal creditAmount, BigDecimal signedBalance) { + return fillAuxiliaryBalance(context, new FmsLedgerAuxiliaryDetailRespVO().setRowType(rowType) + .setSubjectId(context.selectedSubjectId).setPeriod(period.toString()) + .setAccountDate(period.atEndOfMonth()).setDigest(digest) + .setDebitAmount(debitAmount).setCreditAmount(creditAmount), signedBalance); + } + + /** + * 填充核算项目明细账余额 + * + * @param context 辅助核算账簿计算上下文 + * @param result 核算项目明细账行 + * @param signedBalance 余额方向金额 + * @return 核算项目明细账行 + */ + private FmsLedgerAuxiliaryDetailRespVO fillAuxiliaryBalance( + AuxiliaryLedgerContext context, FmsLedgerAuxiliaryDetailRespVO result, BigDecimal signedBalance) { + // 1. 按科目方向展示时,余额方向固定为科目方向,反向净额保留负数 + FmsSubjectDO subject = context.subjectMap.get(context.selectedSubjectId); + if (subject != null && ObjUtil.equal(context.financeParameter.getLedgerBalanceMode(), + FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode())) { + boolean debitSubject = ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()); + return result.setBalanceDirection(signedBalance.signum() == 0 + ? FmsBalanceDirectionEnum.FLAT.getName() + : FmsDebitCreditDirectionEnum.valueOf(subject.getBalanceDirection()).getName()) + .setBalance(debitSubject ? signedBalance : signedBalance.negate()); + } + // 2. 按实际方向展示;未指定科目时可能聚合不同余额方向的科目 + return result.setBalanceDirection(balanceDirection(signedBalance)).setBalance(signedBalance.abs()); + } + + /** + * 构建核算项目余额 + * + * @param context 辅助核算账簿计算上下文 + * @param item 辅助核算项目 + * @return 核算项目余额 + */ + private FmsLedgerAuxiliaryBalanceRespVO buildAuxiliaryBalance(AuxiliaryLedgerContext context, FmsAuxiliaryItemDO item) { + // 1. 筛选核算项目凭证分录 + List entries = filterAuxiliaryEntries(context, item.getId()); + LocalDateTime startTime = context.startMonth.atDay(1).atStartOfDay(); + LocalDateTime endTime = context.endMonth.plusMonths(1).atDay(1).atStartOfDay(); + + // 2. 计算期初和期末余额 + BigDecimal openingSignedAmount = calculateAuxiliaryInitialSignedAmount(context, item.getId()) + .add(calculateSignedAmount(entries, null, startTime)); + List periodEntries = filterEntriesByTime(entries, startTime, endTime); + BigDecimal endingSignedAmount = openingSignedAmount.add(calculateSignedAmount(periodEntries, null, null)); + + // 3. 计算本年累计发生额 + LocalDateTime yearBeginTime = LocalDate.of(context.endMonth.getYear(), 1, 1).atStartOfDay(); + List yearEntries = filterEntriesByTime(entries, yearBeginTime, endTime); + + // 4. 组装核算项目余额响应 + return new FmsLedgerAuxiliaryBalanceRespVO().setAuxiliaryItemId(item.getId()) + .setCode(item.getCode()).setName(item.getName()) + .setOpeningDebitAmount(debitPart(openingSignedAmount)) + .setOpeningCreditAmount(creditPart(openingSignedAmount)) + .setPeriodDebitAmount(sumDebitAmount(periodEntries)) + .setPeriodCreditAmount(sumCreditAmount(periodEntries)) + .setYearDebitAmount(sumDebitAmount(yearEntries).add(calculateAuxiliaryInitialYearAmount( + context, item.getId(), true, context.endMonth.getYear()))) + .setYearCreditAmount(sumCreditAmount(yearEntries).add(calculateAuxiliaryInitialYearAmount( + context, item.getId(), false, context.endMonth.getYear()))) + .setEndingDebitAmount(debitPart(endingSignedAmount)) + .setEndingCreditAmount(creditPart(endingSignedAmount)); + } + + /** + * 筛选指定辅助核算项目的凭证分录 + * + * @param context 辅助核算账簿计算上下文 + * @param auxiliaryItemId 辅助核算项目编号 + * @return 凭证分录列表 + */ + private List filterAuxiliaryEntries(AuxiliaryLedgerContext context, Long auxiliaryItemId) { + return filterList(context.entries, entry -> context.subjectIds.contains(entry.getSubjectId()) + && CollUtil.emptyIfNull(entry.getAuxiliaries()).stream() + .anyMatch(auxiliary -> ObjUtil.equal(auxiliary.getTypeId(), context.auxiliaryTypeId) + && ObjUtil.equal(auxiliary.getItemId(), auxiliaryItemId))); + } + + /** + * 筛选指定时间范围的凭证分录 + * + * @param entries 凭证分录列表 + * @param beginTime 开始时间,为空时不限制 + * @param endTime 结束时间,为空时不限制 + * @return 凭证分录列表 + */ + private List filterEntriesByTime(List entries, + LocalDateTime beginTime, LocalDateTime endTime) { + return filterList(entries, entry -> (beginTime == null || !entry.getVoucherTime().isBefore(beginTime)) + && (endTime == null || entry.getVoucherTime().isBefore(endTime))); + } + + /** + * 计算辅助核算项目初始余额方向金额 + * + * @param context 辅助核算账簿计算上下文 + * @param auxiliaryItemId 辅助核算项目编号 + * @return 初始余额方向金额 + */ + private BigDecimal calculateAuxiliaryInitialSignedAmount(AuxiliaryLedgerContext context, Long auxiliaryItemId) { + BigDecimal result = BigDecimal.ZERO; + for (InitialAssistRecord record : filterInitialAssists(context, auxiliaryItemId)) { + FmsSubjectDO subject = context.subjectMap.get(record.initial.getSubjectId()); + if (subject != null) { + result = result.add(ObjUtil.equal(subject.getBalanceDirection(), + FmsDebitCreditDirectionEnum.DEBIT.getType()) + ? zeroIfNull(record.assist.getOpeningAmount()) + : zeroIfNull(record.assist.getOpeningAmount()).negate()); + } + } + return result; + } + + /** + * 计算辅助核算项目在账套启用年度的年初累计发生额 + * + * @param context 辅助核算账簿计算上下文 + * @param auxiliaryItemId 辅助核算项目编号 + * @param debit 是否借方 + * @param year 年度 + * @return 年初累计发生额 + */ + private BigDecimal calculateAuxiliaryInitialYearAmount(AuxiliaryLedgerContext context, + Long auxiliaryItemId, boolean debit, int year) { + if (context.accountSet.getStartTime().getYear() != year) { + return BigDecimal.ZERO; + } + return sumBigDecimal(filterInitialAssists(context, auxiliaryItemId), + record -> debit ? record.assist.getYearDebitAmount() : record.assist.getYearCreditAmount()); + } + + /** + * 筛选辅助核算项目的初始余额记录 + * + * @param context 辅助核算账簿计算上下文 + * @param auxiliaryItemId 辅助核算项目编号 + * @return 初始余额记录列表 + */ + private List filterInitialAssists(AuxiliaryLedgerContext context, Long auxiliaryItemId) { + return convertListByFlatMap(filterList(context.initialBalances, + initial -> context.subjectIds.contains(initial.getSubjectId()) + && CollUtil.isNotEmpty(initial.getAssistBalances())), initial -> + initial.getAssistBalances().stream() + .filter(assist -> CollUtil.isNotEmpty(assist.getAuxiliaries()) + && assist.getAuxiliaries().stream().anyMatch(auxiliary -> + ObjUtil.equal(auxiliary.getTypeId(), context.auxiliaryTypeId) + && ObjUtil.equal(auxiliary.getItemId(), auxiliaryItemId))) + .map(assist -> new InitialAssistRecord(initial, assist))); + } + + /** + * 校验辅助核算项目存在 + * + * @param context 辅助核算账簿计算上下文 + * @param auxiliaryItemId 辅助核算项目编号 + */ + private void validateAuxiliaryItem(AuxiliaryLedgerContext context, Long auxiliaryItemId) { + if (context.auxiliaryItems.stream().noneMatch(item -> ObjUtil.equal(item.getId(), auxiliaryItemId))) { + throw exception(AUXILIARY_ITEM_NOT_EXISTS); + } + } + + /** + * 收集科目及其全部下级科目编号 + * + * @param childMap 父科目编号与子科目编号 Map + * @param subjectId 科目编号 + * @return 科目编号集合 + */ + private Set collectSubjectIds(Map> childMap, Long subjectId) { + Set result = new LinkedHashSet<>(); + result.add(subjectId); + for (Long childId : childMap.getOrDefault(subjectId, Collections.emptyList())) { + result.addAll(collectSubjectIds(childMap, childId)); + } + return result; + } + + /** + * 将科目余额树转换为数量金额总账树 + * + * @param balances 科目余额树 + * @return 数量金额总账树 + */ + private List buildQuantityGeneralRespVOList(List balances) { + List result = new ArrayList<>(balances.size()); + balances.forEach(balance -> { + FmsLedgerQuantityGeneralRespVO item = BeanUtils.toBean(balance, FmsLedgerQuantityGeneralRespVO.class) + .setOpeningAmount(firstAmount(balance.getOpeningDebitAmount(), balance.getOpeningCreditAmount())) + .setEndingAmount(firstAmount(balance.getEndingDebitAmount(), balance.getEndingCreditAmount())) + .setChildren(buildQuantityGeneralRespVOList(balance.getChildren())); + result.add(item); + }); + return result; + } + + private static BigDecimal firstAmount(BigDecimal debitAmount, BigDecimal creditAmount) { + return debitAmount != null ? debitAmount : creditAmount; + } + + /** + * 普通账簿计算上下文 + */ + @AllArgsConstructor + @SuppressWarnings("ClassCanBeRecord") + private static class LedgerContext { + + /** + * 账套 + */ + private final FmsAccountSetDO accountSet; + /** + * 财务参数 + */ + private final FmsFinanceParameterDO financeParameter; + /** + * 开始会计期间 + */ + private final YearMonth startMonth; + /** + * 结束会计期间 + */ + private final YearMonth endMonth; + /** + * 科目列表 + */ + private final List subjects; + /** + * 科目 Map + */ + private final Map subjectMap; + /** + * 父科目编号与子科目编号 Map + */ + private final Map> childMap; + /** + * 科目初始余额列表 + */ + private final List initialBalances; + /** + * 凭证分录列表 + */ + private final List entries; + } + + /** + * 辅助核算账簿计算上下文 + */ + @AllArgsConstructor + @SuppressWarnings("ClassCanBeRecord") + private static class AuxiliaryLedgerContext { + + /** + * 账套 + */ + private final FmsAccountSetDO accountSet; + /** + * 财务参数 + */ + private final FmsFinanceParameterDO financeParameter; + /** + * 开始会计期间 + */ + private final YearMonth startMonth; + /** + * 结束会计期间 + */ + private final YearMonth endMonth; + /** + * 辅助核算类别编号 + */ + private final Long auxiliaryTypeId; + /** + * 选中的科目编号 + */ + private final Long selectedSubjectId; + /** + * 辅助核算项目列表 + */ + private final List auxiliaryItems; + /** + * 科目 Map + */ + private final Map subjectMap; + /** + * 查询范围内的科目编号集合 + */ + private final Set subjectIds; + /** + * 科目初始余额列表 + */ + private final List initialBalances; + /** + * 凭证分录列表 + */ + private final List entries; + } + + /** + * 辅助核算组合余额计算记录 + */ + @AllArgsConstructor + @SuppressWarnings("ClassCanBeRecord") + private static class AuxiliaryCombinationRecord { + + private final Long id; + private final Long subjectId; + private final List items; + private final List initialAssists; + } + + /** + * 辅助核算项目快照 + */ + @AllArgsConstructor + @SuppressWarnings("ClassCanBeRecord") + private static class AuxiliarySnapshot { + + private final Long itemId; + private final String name; + } + + /** + * 科目初始余额与辅助核算余额记录 + */ + @AllArgsConstructor + @SuppressWarnings("ClassCanBeRecord") + private static class InitialAssistRecord { + + /** + * 科目初始余额 + */ + private final FmsInitialBalanceDO initial; + /** + * 辅助核算余额 + */ + private final FmsInitialBalanceDO.AssistBalance assist; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetService.java new file mode 100644 index 0000000..caa0457 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetService.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; + +import java.time.YearMonth; +import java.util.List; +import java.util.Map; + +/** + * FMS 资产负债表 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsBalanceSheetService { + + /** + * 获得资产负债表 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 资产负债表 + */ + List getBalanceSheet(FmsReportListReqVO listReqVO, Long userId); + + /** + * 获得资产负债行次金额 Map,供现金流量表取数 + * + * 账套权限由调用方校验;报表快照按查询条件的期间初始化,行次金额按取数区间实时计算 + * + * @param listReqVO 查询条件,用于初始化报表快照 + * @param startMonth 取数开始月份 + * @param endMonth 取数结束月份 + * @param userId 用户编号 + * @return 行次到报表项目的 Map + */ + Map getBalanceSheetLineMap(FmsReportListReqVO listReqVO, + YearMonth startMonth, YearMonth endMonth, Long userId); + + /** + * 检查资产负债表的平衡、初始余额、损益结转和公式覆盖状态 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 检查结果 + */ + FmsBalanceSheetCheckRespVO checkBalanceSheet(FmsReportListReqVO listReqVO, Long userId); + + /** + * 更新资产负债表公式 + * + * @param updateReqVO 公式信息 + * @param userId 用户编号 + */ + void updateBalanceSheetFormula(FmsReportFormulaUpdateReqVO updateReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetServiceImpl.java new file mode 100644 index 0000000..f7d912f --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetServiceImpl.java @@ -0,0 +1,458 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.balance.FmsBalanceSheetConfigDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.balance.FmsBalanceSheetReportDO; +import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetConfigMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetReportMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.util.FmsPeriodUtils; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_CONFIG_NOT_EXISTS; + +/** + * FMS 资产负债表 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsBalanceSheetServiceImpl implements FmsBalanceSheetService { + + /** + * 货币资金行次,现金流量表的现金余额取数行 + */ + public static final Integer MONETARY_CASH_ROW_NO = 1; + /** + * 资产总计行次,资产侧与负债侧的分界 + */ + public static final Integer ASSET_TOTAL_ROW_NO = 30; + /** + * 负债和所有者权益总计行次,与资产总计勾稽平衡 + */ + public static final Integer LIABILITY_TOTAL_ROW_NO = 53; + /** + * 表头行的行次,例如“流动资产:”“流动负债:”,正文行次均大于该值 + */ + private static final int HEADER_ROW_NO = 0; + /** + * 负债和所有者权益侧表头的最小显示顺序,资产侧表头和小计的显示顺序均小于该值 + */ + private static final int LIABILITY_SIDE_START_SORT = 32; + /** + * 资本化支出科目编码,余额不纳入资产负债表公式覆盖检查 + */ + private static final String CAPITALIZED_EXPENSE_SUBJECT_CODE = "4404"; + + @Resource + private FmsBalanceSheetConfigMapper balanceSheetConfigMapper; + @Resource + private FmsBalanceSheetReportMapper balanceSheetReportMapper; + @Resource + private FmsReportTemplateMapper reportTemplateMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsReportCommonService reportCommonService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsInitialBalanceService initialBalanceService; + @Resource + private FmsFinanceParameterService financeParameterService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private FmsClosingPeriodService closingPeriodService; + + @Override + @Transactional(rollbackFor = Exception.class) + public List getBalanceSheet(FmsReportListReqVO listReqVO, Long userId) { + // 1.1 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission( + listReqVO.getAccountSetId(), userId); + // 1.2 加载科目并初始化报表配置 + List subjects = subjectService.getSubjectList(listReqVO.getAccountSetId(), null, userId); + List configs = getOrInitBalanceConfigs(listReqVO.getAccountSetId(), subjects); + List reports = getOrCreateBalanceReports(listReqVO, configs); + + // 2. 查询年初至期末的科目余额 + YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth()); + Map balanceMap = reportCommonService.getSubjectBalanceMap( + listReqVO.getAccountSetId(), FmsPeriodUtils.getYearStartMonth(accountSet, endMonth), endMonth, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + + // 3. 计算报表项目,并按行编号配对资产与负债两侧 + Map lineMap = new LinkedHashMap<>(); + Map rowMap = new LinkedHashMap<>(); + for (FmsBalanceSheetReportDO report : reports) { + FmsBalanceSheetConfigDO config = BeanUtils.toBean(report, FmsBalanceSheetConfigDO.class); + FmsReportItemRespVO item = buildBalanceItem(config, subjectMap, balanceMap, lineMap); + if (config.getRowNo() > HEADER_ROW_NO) { + lineMap.put(config.getRowNo(), item); + } + FmsBalanceSheetRowRespVO row = rowMap.computeIfAbsent(config.getRowId(), + rowId -> new FmsBalanceSheetRowRespVO().setRowId(rowId)); + fillRowItem(row, item, isAssetItem(config)); + } + return new ArrayList<>(rowMap.values()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map getBalanceSheetLineMap(FmsReportListReqVO listReqVO, + YearMonth startMonth, YearMonth endMonth, Long userId) { + // 1. 加载科目并初始化报表配置,报表快照按查询期间只初始化一次 + List subjects = subjectService.getSubjectList(listReqVO.getAccountSetId(), null, userId); + List configs = getOrInitBalanceConfigs(listReqVO.getAccountSetId(), subjects); + List reports = getOrCreateBalanceReports(listReqVO, configs); + List reportConfigs = convertList( + reports, report -> BeanUtils.toBean(report, FmsBalanceSheetConfigDO.class)); + + // 2. 查询取数区间的科目余额 + Map balanceMap = reportCommonService.getSubjectBalanceMap( + listReqVO.getAccountSetId(), startMonth, endMonth, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + + // 3. 计算各报表行次金额 + Map lineMap = new LinkedHashMap<>(); + for (FmsBalanceSheetConfigDO config : reportConfigs) { + FmsReportItemRespVO item = buildBalanceItem(config, subjectMap, balanceMap, lineMap); + if (config.getRowNo() > HEADER_ROW_NO) { + lineMap.put(config.getRowNo(), item); + } + } + return lineMap; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FmsBalanceSheetCheckRespVO checkBalanceSheet(FmsReportListReqVO listReqVO, Long userId) { + // 1. 检查资产负债表平衡状态 + List rows = getBalanceSheet(listReqVO, userId); + FmsBalanceSheetRowRespVO assetTotalRow = CollUtil.findOne(rows, + row -> ObjUtil.equal(row.getAssetRowNo(), ASSET_TOTAL_ROW_NO)); + FmsBalanceSheetRowRespVO liabilityTotalRow = CollUtil.findOne(rows, + row -> ObjUtil.equal(row.getLiabilityRowNo(), LIABILITY_TOTAL_ROW_NO)); + BigDecimal openingDifferenceAmount = NumberUtils.zeroIfNull( + assetTotalRow == null ? null : assetTotalRow.getAssetOpeningAmount()) + .subtract(NumberUtils.zeroIfNull( + liabilityTotalRow == null ? null : liabilityTotalRow.getLiabilityOpeningAmount())); + BigDecimal closingDifferenceAmount = NumberUtils.zeroIfNull( + assetTotalRow == null ? null : assetTotalRow.getAssetClosingAmount()) + .subtract(NumberUtils.zeroIfNull( + liabilityTotalRow == null ? null : liabilityTotalRow.getLiabilityClosingAmount())); + + // 2. 检查初始余额和损益结转状态 + boolean initialBalanceBalanced = Boolean.TRUE.equals( + initialBalanceService.getTrialBalance(listReqVO.getAccountSetId(), userId).getBalanced()); + boolean profitLossTransferred = isProfitLossTransferred(listReqVO, userId); + + // 3. 检查未纳入报表公式的非损益类科目 + List subjects = subjectService.getSubjectList(listReqVO.getAccountSetId(), null, userId); + FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(listReqVO.getAccountSetId()); + String capitalizedExpenseSubjectCode = financeParameterService.convertStandardSubjectCode( + CAPITALIZED_EXPENSE_SUBJECT_CODE, financeParameter.getSubjectCodeRule()); + Set formulaSubjectIds = collectRowFormulaSubjectIds(rows); + Map balanceMap = reportCommonService.getSubjectBalanceMap( + listReqVO.getAccountSetId(), LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth()), + LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth()), userId); + List unmappedSubjects = new ArrayList<>(); + for (FmsSubjectDO subject : subjects) { + // 一级非损益科目存在余额且未纳入公式时,需要提示补充报表公式;资本化支出科目除外 + if (!reportCommonService.isRootSubject(subject) + || ObjUtil.equal(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || StrUtil.equals(subject.getCode(), capitalizedExpenseSubjectCode) + || reportCommonService.isSubjectTreeMapped(subject, subjects, formulaSubjectIds) + || !hasBalance(balanceMap.get(subject.getId()))) { + continue; + } + unmappedSubjects.add(new FmsBalanceSheetCheckRespVO.UnmappedSubject() + .setId(subject.getId()).setCode(subject.getCode()).setName(subject.getName())); + } + return new FmsBalanceSheetCheckRespVO() + .setBalanced(openingDifferenceAmount.signum() == 0 && closingDifferenceAmount.signum() == 0) + .setInitialBalanceBalanced(initialBalanceBalanced) + .setProfitLossTransferred(profitLossTransferred) + .setOpeningDifferenceAmount(openingDifferenceAmount) + .setClosingDifferenceAmount(closingDifferenceAmount) + .setUnmappedSubjects(unmappedSubjects); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SuppressWarnings("DataFlowIssue") + public void updateBalanceSheetFormula(FmsReportFormulaUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验报表项目 + FmsBalanceSheetReportDO report = balanceSheetReportMapper.selectById(updateReqVO.getId()); + reportCommonService.validateReportItemEditable(updateReqVO.getAccountSetId(), + report == null ? null : report.getAccountSetId(), report == null ? null : report.getEditable()); + List configs = balanceSheetConfigMapper.selectListByAccountSetId( + updateReqVO.getAccountSetId()); + FmsBalanceSheetConfigDO config = CollUtil.findOne(configs, item -> + ObjUtil.equal(item.getName(), report.getName()) + && ObjUtil.equal(item.getRowNo(), report.getRowNo()) + && ObjUtil.equal(item.getLevel(), report.getLevel()) + && ObjUtil.equal(item.getSort(), report.getSort())); + if (config == null) { + throw exception(REPORT_CONFIG_NOT_EXISTS); + } + + // 2. 校验并构造资产负债表公式 + List formulas = reportCommonService.buildReportFormulaList(updateReqVO, + FmsFormulaRuleEnum.BALANCE.getRule(), FmsFormulaRuleEnum.SUBJECT_CREDIT_BALANCE.getRule(), userId); + + // 3. 更新当前期间和未出报表期间使用的公式 + String formula = JsonUtils.toJsonString(formulas); + balanceSheetReportMapper.updateById(new FmsBalanceSheetReportDO().setId(report.getId()).setFormula(formula)); + balanceSheetConfigMapper.updateById(new FmsBalanceSheetConfigDO().setId(config.getId()).setFormula(formula)); + } + + /** + * 判断报表配置是否属于资产侧 + * + * 行次大于表头行次且不超过资产总计行次的为正文资产项;行次为表头行次时,按显示顺序是否早于负债侧表头判断 + * + * @param config 报表配置 + * @return 是否资产侧 + */ + private boolean isAssetItem(FmsBalanceSheetConfigDO config) { + return config.getRowNo() > HEADER_ROW_NO && config.getRowNo() <= ASSET_TOTAL_ROW_NO + || config.getRowNo() == HEADER_ROW_NO && config.getSort() < LIABILITY_SIDE_START_SORT; + } + + /** + * 将报表项目填充到配对行的资产侧或负债侧字段 + * + * @param row 报表行 + * @param item 报表项目 + * @param asset 是否资产侧 + */ + private void fillRowItem(FmsBalanceSheetRowRespVO row, FmsReportItemRespVO item, boolean asset) { + if (asset) { + row.setAssetId(item.getId()).setAssetName(item.getName()).setAssetRowNo(item.getRowNo()) + .setAssetClosingAmount(item.getClosingAmount()).setAssetOpeningAmount(item.getOpeningAmount()) + .setAssetLevel(item.getLevel()).setAssetEditable(item.getEditable()) + .setAssetFormula(item.getFormula()); + } else { + row.setLiabilityId(item.getId()).setLiabilityName(item.getName()) + .setLiabilityRowNo(item.getRowNo()).setLiabilityClosingAmount(item.getClosingAmount()) + .setLiabilityOpeningAmount(item.getOpeningAmount()).setLiabilityLevel(item.getLevel()) + .setLiabilityEditable(item.getEditable()).setLiabilityFormula(item.getFormula()); + } + } + + /** + * 判断查询期间内各月的损益是否均已结转,逐月检查结账概览的损益余额 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 是否已结转 + */ + private boolean isProfitLossTransferred(FmsReportListReqVO listReqVO, Long userId) { + YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth()); + for (YearMonth currentMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth()); + !currentMonth.isAfter(endMonth); currentMonth = currentMonth.plusMonths(1)) { + FmsClosingQueryReqVO closingQueryReqVO = new FmsClosingQueryReqVO(); + closingQueryReqVO.setAccountSetId(listReqVO.getAccountSetId()); + closingQueryReqVO.setMonth(currentMonth.toString()); + FmsClosingOverviewRespVO overview = closingPeriodService.getClosingOverview(closingQueryReqVO, userId); + // 当月损益余额不为零时,说明存在尚未结转的损益 + if (NumberUtils.zeroIfNull(overview.getProfitLossBalance()).signum() != 0) { + return false; + } + } + return true; + } + + /** + * 收集报表各行公式中引用的科目编号集合,用于公式覆盖检查 + * + * @param rows 报表行 + * @return 公式中的科目编号集合 + */ + private Set collectRowFormulaSubjectIds(List rows) { + Set subjectIds = new LinkedHashSet<>(); + for (FmsBalanceSheetRowRespVO row : rows) { + subjectIds.addAll(reportCommonService.parseFormulaSubjectIds(row.getAssetFormula())); + subjectIds.addAll(reportCommonService.parseFormulaSubjectIds(row.getLiabilityFormula())); + } + return subjectIds; + } + + /** + * 判断科目是否存在期初或期末余额 + * + * @param balance 科目余额 + * @return 是否存在余额 + */ + private boolean hasBalance(FmsLedgerSubjectBalanceRespVO balance) { + return balance != null && (NumberUtils.zeroIfNull(balance.getOpeningDebitAmount()).signum() != 0 + || NumberUtils.zeroIfNull(balance.getOpeningCreditAmount()).signum() != 0 + || NumberUtils.zeroIfNull(balance.getEndingDebitAmount()).signum() != 0 + || NumberUtils.zeroIfNull(balance.getEndingCreditAmount()).signum() != 0); + } + + /** + * 计算报表项目的期初和期末金额 + * + * 行次公式直接引用已计算的行次金额;科目公式逐项取数并按运算符累加,取数结果回写公式项用于前端展示 + * + * @param config 报表配置 + * @param subjectMap 科目编号到科目的 Map + * @param balanceMap 科目编号到余额的 Map + * @param lineMap 行次到已计算报表项目的 Map + * @return 报表项目 + */ + private FmsReportItemRespVO buildBalanceItem(FmsBalanceSheetConfigDO config, + Map subjectMap, + Map balanceMap, + Map lineMap) { + FmsReportItemRespVO item = new FmsReportItemRespVO().setId(config.getId()).setName(config.getName()) + .setRowNo(config.getRowNo()).setLevel(config.getLevel()).setEditable(config.getEditable()) + .setFormula(config.getFormula()).setOpeningAmount(BigDecimal.ZERO) + .setClosingAmount(BigDecimal.ZERO); + // 行次公式直接引用前面已计算的行次金额,例如 ["L1-L2"] + if (reportCommonService.isLineFormula(config.getFormula())) { + item.setOpeningAmount(reportCommonService.calculateItemLineFormula(config.getFormula(), lineMap, true, false)) + .setClosingAmount(reportCommonService.calculateItemLineFormula(config.getFormula(), lineMap, false, false)); + return item; + } + // 科目公式逐项取数,按运算符累加到期初、期末金额 + List formulas = reportCommonService.parseSubjectFormula(config.getFormula()); + for (FmsReportFormulaRespVO formula : formulas) { + FmsSubjectDO subject = subjectMap.get(formula.getSubjectId()); + reportCommonService.normalizeSubjectFormula(formula, subject); + BigDecimal openingAmount = reportCommonService.calculateBalanceAmount( + formula.getRules(), subject, balanceMap, subjectMap, true); + BigDecimal closingAmount = reportCommonService.calculateBalanceAmount( + formula.getRules(), subject, balanceMap, subjectMap, false); + formula.setOpeningAmount(openingAmount).setClosingAmount(closingAmount); + item.setOpeningAmount(reportCommonService.applyOperator(item.getOpeningAmount(), openingAmount, formula.getOperator())) + .setClosingAmount(reportCommonService.applyOperator(item.getClosingAmount(), closingAmount, formula.getOperator())); + } + // 回写含取数结果的公式,供前端公式编辑弹窗展示各项金额 + item.setFormula(JsonUtils.toJsonString(formulas)); + return item; + } + + /** + * 获得账套的报表配置,首次查询时按报表模板初始化 + * + * 初始化时将模板公式中的科目编码绑定为账套下的科目编号,编码不存在的科目编号置空 + * + * @param accountSetId 账套编号 + * @param subjects 账套下的科目列表 + * @return 报表配置列表 + */ + private List getOrInitBalanceConfigs(Long accountSetId, List subjects) { + // 1. 已初始化时直接返回 + List configs = balanceSheetConfigMapper.selectListByAccountSetId(accountSetId); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 2. 锁定账套后二次查询,避免并发首次查询重复初始化 + accountSetService.lockAccountSet(accountSetId); + configs = balanceSheetConfigMapper.selectListByAccountSetId(accountSetId); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 3. 按报表模板初始化,模板为空时返回空列表 + List templates = reportTemplateMapper.selectListByType( + FmsReportTypeEnum.BALANCE_SHEET.getType()); + if (CollUtil.isEmpty(templates)) { + return Collections.emptyList(); + } + // 4. 模板公式中的科目编码绑定为账套下的科目编号后批量插入 + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode, + Function.identity(), (first, second) -> first); + configs = convertList(templates, template -> BeanUtils.toBean(template, FmsBalanceSheetConfigDO.class) + .setId(null).setAccountSetId(accountSetId) + .setFormula(reportCommonService.bindSubjectFormula(template.getFormula(), subjectMap))); + balanceSheetConfigMapper.insertBatch(configs); + return configs; + } + + /** + * 获得查询期间的报表快照,首次查询该期间时按当前配置创建 + * + * @param listReqVO 查询条件 + * @param configs 报表配置列表 + * @return 报表快照列表 + */ + private List getOrCreateBalanceReports( + FmsReportListReqVO listReqVO, List configs) { + // 1. 已生成快照时直接返回;配置为空时不生成 + Integer fromPeriod = reportCommonService.parsePeriod(listReqVO.getStartMonth()); + Integer toPeriod = reportCommonService.parsePeriod(listReqVO.getEndMonth()); + Integer periodType = reportCommonService.getPeriodType(fromPeriod, toPeriod); + List reports = balanceSheetReportMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType); + if (CollUtil.isNotEmpty(reports) || CollUtil.isEmpty(configs)) { + return reports; + } + // 2. 锁定账套后二次查询,避免并发首次查询重复生成期间快照 + accountSetService.lockAccountSet(listReqVO.getAccountSetId()); + reports = balanceSheetReportMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType); + if (CollUtil.isNotEmpty(reports)) { + return reports; + } + // 3. 按当前配置创建期间快照,金额初始化为零、未结账 + reports = convertList(configs, config -> BeanUtils.toBean(config, FmsBalanceSheetReportDO.class) + .setId(null).setFromPeriod(fromPeriod).setToPeriod(toPeriod).setType(periodType) + .setOpeningAmount(BigDecimal.ZERO).setClosingAmount(BigDecimal.ZERO).setSettled(false)); + balanceSheetReportMapper.insertBatch(reports); + return reports; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementService.java new file mode 100644 index 0000000..0e99f03 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementService.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowStatementUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; + +import java.util.List; + +/** + * FMS 现金流量表 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsCashFlowStatementService { + + /** + * 获得现金流量表 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 现金流量表 + */ + List getCashFlowStatement(FmsReportListReqVO listReqVO, Long userId); + + /** + * 更新现金流量表 + * + * @param updateReqVO 修改信息 + * @param userId 用户编号 + */ + void updateCashFlowStatement(FmsCashFlowStatementUpdateReqVO updateReqVO, Long userId); + + /** + * 检查现金流量表取数依赖的资产负债表状态 + * + * 现金流量表本身无独立平衡关系,检查结果复用资产负债表检查,并汇总是否满足取数条件 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 检查结果 + */ + FmsCashFlowCheckRespVO checkCashFlowStatement(FmsReportListReqVO listReqVO, Long userId); + + /** + * 获得现金流量辅助数据列表 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 现金流量辅助数据列表 + */ + List getCashFlowAdjustmentList(FmsReportListReqVO listReqVO, Long userId); + + /** + * 更新现金流量辅助数据的人工调整金额 + * + * @param updateReqVO 辅助数据 + * @param userId 用户编号 + */ + void updateCashFlowAdjustment(FmsCashFlowAdjustmentUpdateReqVO updateReqVO, Long userId); + + /** + * 更新现金流量辅助数据公式 + * + * @param updateReqVO 公式信息 + * @param userId 用户编号 + */ + void updateCashFlowAdjustmentFormula(FmsReportFormulaUpdateReqVO updateReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementServiceImpl.java new file mode 100644 index 0000000..888aa1b --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementServiceImpl.java @@ -0,0 +1,704 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowStatementUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowExtendConfigDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowExtendDataDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowStatementConfigDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowStatementReportDO; +import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow.FmsCashFlowExtendConfigMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow.FmsCashFlowExtendDataMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow.FmsCashFlowStatementConfigMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow.FmsCashFlowStatementReportMapper; +import cn.iocoder.yudao.module.fms.enums.report.FmsCashFlowExtendTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsCashFlowParamTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.util.FmsPeriodUtils; +import com.googlecode.aviator.AviatorEvaluator; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_ADJUSTMENT_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_AMOUNT_NOT_ADJUSTABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_CONFIG_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_CONFIG_NOT_EDITABLE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CASH_FLOW_ADJUSTMENT_FORMULA_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CASH_FLOW_ADJUSTMENT_FORMULA_UPDATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CASH_FLOW_ADJUSTMENT_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CASH_FLOW_ADJUSTMENT_UPDATE_SUCCESS; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CASH_FLOW_STATEMENT_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CASH_FLOW_STATEMENT_UPDATE_SUB_TYPE; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.FMS_CASH_FLOW_STATEMENT_UPDATE_SUCCESS; + +/** + * FMS 现金流量表 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsCashFlowStatementServiceImpl implements FmsCashFlowStatementService { + + /** + * 现金流量表主表分类 + */ + private static final Integer CASH_FLOW_CATEGORY_MAIN = 1; + /** + * 其他经营活动现金行次,未明确归类的经营活动现金流归入该行 + */ + private static final int OTHER_OPERATING_ROW_NO = 6; + /** + * 期末现金及现金等价物余额行次 + */ + private static final int ENDING_CASH_ROW_NO = 22; + + private static final Pattern LINE_FORMULA_PATTERN = Pattern.compile("([+-]?)(L\\d+)"); + private static final Pattern CASH_FLOW_PARAM_PATTERN = Pattern.compile("BA\\[\\d+,[12]]|IN\\d+|EX\\d+|L\\d+"); + private static final Pattern CASH_FLOW_EXPRESSION_PATTERN = Pattern.compile("[A-Z0-9_+\\-*/().?:<>=\\s]+"); + + @Resource + private FmsCashFlowStatementConfigMapper cashFlowStatementConfigMapper; + @Resource + private FmsCashFlowStatementReportMapper cashFlowStatementReportMapper; + @Resource + private FmsCashFlowExtendConfigMapper cashFlowExtendConfigMapper; + @Resource + private FmsCashFlowExtendDataMapper cashFlowExtendDataMapper; + @Resource + private FmsReportTemplateMapper reportTemplateMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsReportCommonService reportCommonService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsBalanceSheetService balanceSheetService; + @Resource + private FmsIncomeStatementService incomeStatementService; + + @Override + @Transactional(rollbackFor = Exception.class) + public List getCashFlowStatement(FmsReportListReqVO listReqVO, Long userId) { + // 1.1 校验账套读权限 + FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission( + listReqVO.getAccountSetId(), userId); + // 1.2 初始化报表配置 + List configs = getOrInitCashFlowConfigs(listReqVO.getAccountSetId()); + List reports = getOrCreateCashFlowReports(listReqVO, configs); + + // 2. 查询本期和本年的资产负债、利润表和辅助数据取数 + YearMonth startMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth()); + YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth()); + Map currentBalanceMap = balanceSheetService.getBalanceSheetLineMap( + listReqVO, startMonth, endMonth, userId); + Map yearBalanceMap = balanceSheetService.getBalanceSheetLineMap( + listReqVO, FmsPeriodUtils.getYearStartMonth(accountSet, endMonth), endMonth, userId); + List incomeItems = incomeStatementService.getIncomeStatement(listReqVO, userId); + Map incomeMap = convertMap( + filterList(incomeItems, item -> item.getRowNo() != null && item.getRowNo() > 0), + FmsReportItemRespVO::getRowNo, Function.identity(), (first, second) -> first); + List adjustments = getCashFlowAdjustmentListInternal(listReqVO, userId); + Map adjustmentMap = convertMap( + filterList(adjustments, item -> item.getRowNo() != null && item.getRowNo() > 0), + FmsCashFlowAdjustmentRespVO::getRowNo, Function.identity(), (first, second) -> first); + + // 3. 计算现金流量表项目 + Map reportMap = convertMap( + filterList(reports, report -> report.getRowNo() != null && report.getRowNo() > 0), + FmsCashFlowStatementReportDO::getRowNo, Function.identity(), (first, second) -> first); + Map lineMap = new LinkedHashMap<>(); + List result = new ArrayList<>(); + for (FmsCashFlowStatementReportDO report : reports) { + FmsCashFlowStatementConfigDO config = BeanUtils.toBean(report, FmsCashFlowStatementConfigDO.class); + FmsReportItemRespVO item = BeanUtils.toBean(report, FmsReportItemRespVO.class); + BigDecimal currentAmount = calculateCashFlowFormula(config.getFormula(), currentBalanceMap, + incomeMap, adjustmentMap, lineMap, true); + BigDecimal yearAmount = calculateCashFlowFormula(config.getFormula(), yearBalanceMap, + incomeMap, adjustmentMap, lineMap, false); + if (isAmountAdjustable(report)) { + currentAmount = getSavedAmount(report.getCurrentAmount(), currentAmount); + yearAmount = getSavedAmount(report.getYearAmount(), yearAmount); + } + item.setCurrentAmount(currentAmount).setYearAmount(yearAmount); + if (config.getRowNo() != null && config.getRowNo() > 0) { + lineMap.put(config.getRowNo(), item); + } + result.add(item); + } + + // 4. 将未明确归类的经营活动现金流归入其他经营活动 + reconcileCashFlow(result, lineMap, currentBalanceMap, yearBalanceMap, adjustmentMap, reportMap); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CASH_FLOW_STATEMENT_TYPE, + subType = FMS_CASH_FLOW_STATEMENT_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.accountSetId}}", success = FMS_CASH_FLOW_STATEMENT_UPDATE_SUCCESS) + public void updateCashFlowStatement(FmsCashFlowStatementUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验现金流量表项目属于指定期间 + Integer fromPeriod = reportCommonService.parsePeriod(updateReqVO.getStartMonth()); + Integer toPeriod = reportCommonService.parsePeriod(updateReqVO.getEndMonth()); + Integer periodType = reportCommonService.getPeriodType(fromPeriod, toPeriod); + List ids = convertList(updateReqVO.getItems(), FmsCashFlowStatementUpdateReqVO.Item::getId); + List reports = cashFlowStatementReportMapper.selectListByIdsAndPeriod( + ids, updateReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType); + Map reportMap = convertMap( + reports, FmsCashFlowStatementReportDO::getId); + if (reportMap.size() != new LinkedHashSet<>(ids).size()) { + throw exception(REPORT_CONFIG_NOT_EXISTS); + } + // 1.3 校验报表项目允许调整金额 + for (FmsCashFlowStatementUpdateReqVO.Item item : updateReqVO.getItems()) { + if (!isAmountAdjustable(reportMap.get(item.getId()))) { + throw exception(REPORT_AMOUNT_NOT_ADJUSTABLE); + } + } + + // 2. 更新现金流量表金额,金额为零时重新按公式计算 + cashFlowStatementReportMapper.updateBatch(convertList(updateReqVO.getItems(), item -> + new FmsCashFlowStatementReportDO().setId(item.getId()) + .setCurrentAmount(item.getCurrentAmount()) + .setYearAmount(item.getYearAmount()))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FmsCashFlowCheckRespVO checkCashFlowStatement(FmsReportListReqVO listReqVO, Long userId) { + // 现金流量表本身无独立平衡关系,直接复用资产负债表检查,并汇总是否满足取数条件 + FmsBalanceSheetCheckRespVO balanceCheck = balanceSheetService.checkBalanceSheet(listReqVO, userId); + return BeanUtils.toBean(balanceCheck, FmsCashFlowCheckRespVO.class) + .setBalanceSheetReady(Boolean.TRUE.equals(balanceCheck.getBalanced()) + && Boolean.TRUE.equals(balanceCheck.getInitialBalanceBalanced()) + && Boolean.TRUE.equals(balanceCheck.getProfitLossTransferred()) + && CollUtil.isEmpty(balanceCheck.getUnmappedSubjects())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getCashFlowAdjustmentList( + FmsReportListReqVO listReqVO, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(listReqVO.getAccountSetId(), userId); + + // 2. 初始化并计算辅助数据 + return getCashFlowAdjustmentListInternal(listReqVO, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CASH_FLOW_STATEMENT_TYPE, + subType = FMS_CASH_FLOW_ADJUSTMENT_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.accountSetId}}", success = FMS_CASH_FLOW_ADJUSTMENT_UPDATE_SUCCESS) + public void updateCashFlowAdjustment( + FmsCashFlowAdjustmentUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验现金流量辅助数据 + List ids = convertList(updateReqVO.getItems(), FmsCashFlowAdjustmentUpdateReqVO.Item::getId); + List dataList = cashFlowExtendDataMapper.selectListByIdsAndAccountSetId( + ids, updateReqVO.getAccountSetId()); + Map dataMap = convertMap(dataList, FmsCashFlowExtendDataDO::getId); + if (dataMap.size() != new LinkedHashSet<>(ids).size()) { + throw exception(REPORT_ADJUSTMENT_NOT_EXISTS); + } + // 1.3 校验辅助数据允许编辑 + for (FmsCashFlowAdjustmentUpdateReqVO.Item item : updateReqVO.getItems()) { + if (Boolean.FALSE.equals(dataMap.get(item.getId()).getEditable())) { + throw exception(REPORT_CONFIG_NOT_EDITABLE); + } + } + + // 2. 批量更新可编辑金额 + cashFlowExtendDataMapper.updateBatch(convertList(updateReqVO.getItems(), + item -> new FmsCashFlowExtendDataDO().setId(item.getId()) + .setCurrentAmount(item.getCurrentAmount()).setYearAmount(item.getYearAmount()))); + + // 3. 辅助资料变化后,主表人工调整失效,下次查询重新按公式计算 + FmsCashFlowExtendDataDO periodData = CollUtil.getFirst(dataList); + cashFlowStatementReportMapper.clearAdjustedAmounts(updateReqVO.getAccountSetId(), + periodData.getFromPeriod(), periodData.getToPeriod(), periodData.getType()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_CASH_FLOW_STATEMENT_TYPE, + subType = FMS_CASH_FLOW_ADJUSTMENT_FORMULA_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.accountSetId}}", success = FMS_CASH_FLOW_ADJUSTMENT_FORMULA_UPDATE_SUCCESS) + public void updateCashFlowAdjustmentFormula(FmsReportFormulaUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验现金流量辅助数据 + FmsCashFlowExtendDataDO data = cashFlowExtendDataMapper.selectById(updateReqVO.getId()); + if (data == null || ObjUtil.notEqual(data.getAccountSetId(), updateReqVO.getAccountSetId())) { + throw exception(REPORT_ADJUSTMENT_NOT_EXISTS); + } + if (Boolean.FALSE.equals(data.getEditable())) { + throw exception(REPORT_CONFIG_NOT_EDITABLE); + } + + // 2. 校验并构造辅助数据公式 + List formulas = reportCommonService.buildReportFormulaList(updateReqVO, + FmsFormulaRuleEnum.DEBIT_AMOUNT.getRule(), + FmsFormulaRuleEnum.PROFIT_LOSS_AMOUNT.getRule(), userId); + + // 3. 更新当前期间公式并清空人工调整金额 + cashFlowExtendDataMapper.updateById(new FmsCashFlowExtendDataDO().setId(data.getId()) + .setFormula(JsonUtils.toJsonString(formulas)) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO)); + } + + /** + * 初始化并计算当前期间的现金流量辅助数据 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 现金流量辅助数据列表 + */ + private List getCashFlowAdjustmentListInternal( + FmsReportListReqVO listReqVO, Long userId) { + // 1. 初始化当前期间辅助数据 + List subjects = subjectService.getSubjectList(listReqVO.getAccountSetId(), null, userId); + List configs = getOrInitCashFlowExtendConfigs(listReqVO.getAccountSetId(), subjects); + Integer fromPeriod = reportCommonService.parsePeriod(listReqVO.getStartMonth()); + Integer toPeriod = reportCommonService.parsePeriod(listReqVO.getEndMonth()); + Integer periodType = reportCommonService.getPeriodType(fromPeriod, toPeriod); + List dataList = cashFlowExtendDataMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType, CASH_FLOW_CATEGORY_MAIN); + if (CollUtil.isEmpty(dataList) && CollUtil.isNotEmpty(configs)) { + accountSetService.lockAccountSet(listReqVO.getAccountSetId()); + dataList = cashFlowExtendDataMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType, CASH_FLOW_CATEGORY_MAIN); + if (CollUtil.isEmpty(dataList)) { + dataList = convertList(configs, config -> BeanUtils.toBean(config, + FmsCashFlowExtendDataDO.class).setId(null) + .setFromPeriod(fromPeriod).setToPeriod(toPeriod).setType(periodType) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO)); + cashFlowExtendDataMapper.insertBatch(dataList); + } + } + + // 2. 查询科目发生额 + Map balanceMap = reportCommonService.getSubjectBalanceMap( + listReqVO.getAccountSetId(), LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth()), + LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth()), userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + + // 3. 计算辅助数据项目 + Map lineMap = new LinkedHashMap<>(); + List result = new ArrayList<>(); + for (FmsCashFlowExtendDataDO data : dataList) { + FmsCashFlowAdjustmentRespVO item = buildCashFlowAdjustmentItem(data, subjectMap, balanceMap, lineMap); + if (data.getRowNo() != null && data.getRowNo() > 0) { + lineMap.put(data.getRowNo(), item); + } + result.add(item); + } + return result; + } + + /** + * 获得账套的现金流量辅助数据配置,首次查询时按报表模板初始化 + * + * 行次公式的辅助数据标记为固定类型,科目公式标记为自定义类型;模板公式中的科目编码绑定为账套下的科目编号 + * + * @param accountSetId 账套编号 + * @param subjects 账套下的科目列表 + * @return 辅助数据配置列表 + */ + private List getOrInitCashFlowExtendConfigs(Long accountSetId, List subjects) { + // 1. 已初始化时直接返回 + List configs = cashFlowExtendConfigMapper.selectListByAccountSetIdAndCategory( + accountSetId, CASH_FLOW_CATEGORY_MAIN); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 2. 锁定账套后二次查询,避免并发首次查询重复初始化 + accountSetService.lockAccountSet(accountSetId); + configs = cashFlowExtendConfigMapper.selectListByAccountSetIdAndCategory( + accountSetId, CASH_FLOW_CATEGORY_MAIN); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 3. 按报表模板初始化,模板为空时返回空列表 + List templates = reportTemplateMapper.selectListByType(FmsReportTypeEnum.CASH_FLOW_ADJUSTMENT.getType()); + if (CollUtil.isEmpty(templates)) { + return Collections.emptyList(); + } + // 4. 模板公式中的科目编码绑定为账套下的科目编号后批量插入 + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode, + Function.identity(), (first, second) -> first); + configs = convertList(templates, template -> BeanUtils.toBean(template, + FmsCashFlowExtendConfigDO.class).setId(null).setAccountSetId(accountSetId) + .setType(reportCommonService.isLineFormula(template.getFormula()) + ? FmsCashFlowExtendTypeEnum.FIXED.getType() + : FmsCashFlowExtendTypeEnum.CUSTOM.getType()) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO) + .setFormula(reportCommonService.bindSubjectFormula(template.getFormula(), subjectMap))); + cashFlowExtendConfigMapper.insertBatch(configs); + return configs; + } + + /** + * 计算辅助数据项目的本期和本年累计金额,人工调整金额非零时覆盖公式计算结果 + * + * @param data 辅助数据 + * @param subjectMap 科目编号到科目的 Map + * @param balanceMap 科目编号到余额的 Map + * @param lineMap 行次到已计算辅助数据项目的 Map + * @return 辅助数据项目 + */ + private FmsCashFlowAdjustmentRespVO buildCashFlowAdjustmentItem( + FmsCashFlowExtendDataDO data, Map subjectMap, + Map balanceMap, + Map lineMap) { + FmsCashFlowAdjustmentRespVO item = new FmsCashFlowAdjustmentRespVO() + .setId(data.getId()).setName(data.getName()).setRowNo(data.getRowNo()) + .setFormula(data.getFormula()).setRemark(data.getRemark()) + .setEditable(data.getEditable()).setLevel(data.getLevel()) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO); + // 行次公式直接引用前面已计算的行次金额,例如 ["L1+L2"] + if (reportCommonService.isLineFormula(data.getFormula())) { + return item.setCurrentAmount(calculateAdjustmentLineFormula(data.getFormula(), lineMap, true)) + .setYearAmount(calculateAdjustmentLineFormula(data.getFormula(), lineMap, false)); + } + // 科目公式逐项取数,按运算符累加到本期、本年累计金额 + List formulas = reportCommonService.parseSubjectFormula(data.getFormula()); + for (FmsReportFormulaRespVO formula : formulas) { + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(formula.getSubjectId()); + FmsSubjectDO subject = subjectMap.get(formula.getSubjectId()); + reportCommonService.normalizeSubjectFormula(formula, subject); + BigDecimal currentAmount = reportCommonService.calculateOccurrenceAmount(formula.getRules(), subject, balance, true); + BigDecimal yearAmount = reportCommonService.calculateOccurrenceAmount(formula.getRules(), subject, balance, false); + formula.setCurrentAmount(currentAmount).setYearAmount(yearAmount); + item.setCurrentAmount(reportCommonService.applyOperator(item.getCurrentAmount(), currentAmount, formula.getOperator())) + .setYearAmount(reportCommonService.applyOperator(item.getYearAmount(), yearAmount, formula.getOperator())); + } + // 人工调整金额非零时覆盖公式计算结果 + if (NumberUtils.zeroIfNull(data.getCurrentAmount()).signum() != 0) { + item.setCurrentAmount(data.getCurrentAmount()); + } + if (NumberUtils.zeroIfNull(data.getYearAmount()).signum() != 0) { + item.setYearAmount(data.getYearAmount()); + } + return item.setFormula(JsonUtils.toJsonString(formulas)); + } + + /** + * 计算辅助数据的行次公式金额,例如 ["L1+L2"] + * + * @param formula 行次公式 + * @param lineMap 行次到已计算辅助数据项目的 Map + * @param current 是否取本期金额,否则取本年累计金额 + * @return 金额 + */ + private BigDecimal calculateAdjustmentLineFormula(String formula, Map lineMap, boolean current) { + // 逐个匹配 L 行次引用,按运算符累加对应行次金额 + String expression = CollUtil.getFirst(reportCommonService.parseLineFormula(formula)); + if (StrUtil.isEmpty(expression)) { + return BigDecimal.ZERO; + } + BigDecimal result = BigDecimal.ZERO; + Matcher matcher = LINE_FORMULA_PATTERN.matcher(expression); + while (matcher.find()) { + FmsCashFlowAdjustmentRespVO item = lineMap.get(Integer.valueOf(matcher.group(2).substring(1))); + BigDecimal amount = item == null ? BigDecimal.ZERO : current + ? NumberUtils.zeroIfNull(item.getCurrentAmount()) + : NumberUtils.zeroIfNull(item.getYearAmount()); + result = reportCommonService.applyOperator(result, amount, matcher.group(1)); + } + return result; + } + + /** + * 计算现金流量表项目的跨报表表达式金额 + * + * 表达式中的 BA(资产负债取数)、IN(利润表取数)、EX(辅助数据取数)和 L(行次取数)参数 + * 替换为变量后交由 Aviator 计算;表达式含非法字符时按零处理 + * + * @param formula 行次公式 + * @param balanceMap 资产负债行次到报表项目的 Map + * @param incomeMap 利润表行次到报表项目的 Map + * @param adjustmentMap 辅助数据行次到项目的 Map + * @param lineMap 行次到已计算报表项目的 Map + * @param current 是否取本期金额,否则取本年累计金额 + * @return 金额 + */ + private BigDecimal calculateCashFlowFormula(String formula, Map balanceMap, + Map incomeMap, + Map adjustmentMap, + Map lineMap, boolean current) { + String expression = CollUtil.getFirst(reportCommonService.parseLineFormula(formula)); + if (StrUtil.isEmpty(expression)) { + return BigDecimal.ZERO; + } + // 1. 将 BA、IN、EX、L 参数逐个替换为 Aviator 变量,并放入对应的取数金额 + Map environment = new HashMap<>(); + Matcher matcher = CASH_FLOW_PARAM_PATTERN.matcher(expression); + StringBuffer normalizedExpression = new StringBuffer(); + while (matcher.find()) { + String param = matcher.group(); + String variable = param.replace("[", "_").replace(",", "_").replace("]", ""); + environment.put(variable, getCashFlowParamValue( + param, balanceMap, incomeMap, adjustmentMap, lineMap, current)); + matcher.appendReplacement(normalizedExpression, variable); + } + matcher.appendTail(normalizedExpression); + // 2. 校验替换后的表达式只含合法字符,避免公式被注入任意代码 + if (!CASH_FLOW_EXPRESSION_PATTERN.matcher(normalizedExpression).matches()) { + return BigDecimal.ZERO; + } + // 3. 交由 Aviator 计算表达式结果 + Object value = AviatorEvaluator.execute(normalizedExpression.toString(), environment); + return value == null ? BigDecimal.ZERO : new BigDecimal(value.toString()); + } + + /** + * 判断现金流量表项目是否允许调整金额 + * + * 表头没有行次,汇总行使用 L 行次公式,两类项目都由系统自动计算 + * + * @param report 现金流量表项目 + * @return 是否允许调整金额 + */ + private boolean isAmountAdjustable(FmsCashFlowStatementReportDO report) { + return report.getRowNo() != null && report.getRowNo() > 0 + && !reportCommonService.isLineFormula(report.getFormula()); + } + + /** + * 获得跨报表表达式单个参数的取数金额,参数缺失时按零处理 + * + * @param param 参数,例如 BA[1,2]、IN32、EX4、L7 + * @param balanceMap 资产负债行次到报表项目的 Map + * @param incomeMap 利润表行次到报表项目的 Map + * @param adjustmentMap 辅助数据行次到项目的 Map + * @param lineMap 行次到已计算报表项目的 Map + * @param current 是否取本期金额,否则取本年累计金额 + * @return 金额 + */ + private BigDecimal getCashFlowParamValue(String param, + Map balanceMap, + Map incomeMap, + Map adjustmentMap, + Map lineMap, boolean current) { + FmsCashFlowParamTypeEnum paramType = FmsCashFlowParamTypeEnum.of(param); + if (paramType == null) { + return BigDecimal.ZERO; + } + // BA[行次,1期初|2期末]:资产负债行次金额 + if (paramType == FmsCashFlowParamTypeEnum.BALANCE) { + String[] parts = param.substring(paramType.getPrefix().length() + 1, param.length() - 1).split(","); + FmsReportItemRespVO item = balanceMap.get(Integer.valueOf(parts[0])); + if (item == null) { + return BigDecimal.ZERO; + } + return "1".equals(parts[1]) ? NumberUtils.zeroIfNull(item.getOpeningAmount()) + : NumberUtils.zeroIfNull(item.getClosingAmount()); + } + // IN行次:利润表行次金额 + if (paramType == FmsCashFlowParamTypeEnum.INCOME) { + FmsReportItemRespVO item = incomeMap.get( + Integer.valueOf(param.substring(paramType.getPrefix().length()))); + return item == null ? BigDecimal.ZERO : current + ? NumberUtils.zeroIfNull(item.getCurrentAmount()) + : NumberUtils.zeroIfNull(item.getYearAmount()); + } + // EX行次:辅助数据行次金额 + if (paramType == FmsCashFlowParamTypeEnum.EXTEND) { + FmsCashFlowAdjustmentRespVO item = adjustmentMap.get( + Integer.valueOf(param.substring(paramType.getPrefix().length()))); + return item == null ? BigDecimal.ZERO : current + ? NumberUtils.zeroIfNull(item.getCurrentAmount()) + : NumberUtils.zeroIfNull(item.getYearAmount()); + } + // L行次:本表已计算行次金额 + FmsReportItemRespVO item = lineMap.get( + Integer.valueOf(param.substring(paramType.getPrefix().length()))); + return item == null ? BigDecimal.ZERO : current + ? NumberUtils.zeroIfNull(item.getCurrentAmount()) + : NumberUtils.zeroIfNull(item.getYearAmount()); + } + + /** + * 获得现金流量表金额 + * + * 既有财务规则使用非零金额覆盖公式计算结果,保存零值时重新按公式计算 + * + * @param savedAmount 已保存金额 + * @param calculatedAmount 公式计算金额 + * @return 现金流量表金额 + */ + private BigDecimal getSavedAmount(BigDecimal savedAmount, BigDecimal calculatedAmount) { + return NumberUtils.zeroIfNull(savedAmount).signum() != 0 ? savedAmount : calculatedAmount; + } + + /** + * 将未明确归类的经营活动现金流归入其他经营活动,并重算受其影响的行次公式项目 + * + * 其他经营活动现金 = 期末现金余额 - 资产负债表现金余额的倒挤差额,差额为正时覆盖原计算值 + * + * @param result 现金流量表项目列表 + * @param lineMap 行次到已计算报表项目的 Map + * @param currentBalanceMap 本期资产负债行次到报表项目的 Map + * @param yearBalanceMap 本年资产负债行次到报表项目的 Map + * @param adjustmentMap 辅助数据行次到项目的 Map + */ + private void reconcileCashFlow(List result, + Map lineMap, + Map currentBalanceMap, + Map yearBalanceMap, + Map adjustmentMap, + Map reportMap) { + FmsReportItemRespVO otherOperating = lineMap.get(OTHER_OPERATING_ROW_NO); + FmsReportItemRespVO endingCash = lineMap.get(ENDING_CASH_ROW_NO); + if (otherOperating == null || endingCash == null) { + return; + } + // 1. 按资产负债表的货币资金期末余额,倒挤其他经营活动现金流 + FmsReportItemRespVO currentCashItem = currentBalanceMap.get(FmsBalanceSheetServiceImpl.MONETARY_CASH_ROW_NO); + FmsReportItemRespVO yearCashItem = yearBalanceMap.get(FmsBalanceSheetServiceImpl.MONETARY_CASH_ROW_NO); + BigDecimal currentCash = currentCashItem == null ? BigDecimal.ZERO + : NumberUtils.zeroIfNull(currentCashItem.getClosingAmount()); + BigDecimal yearCash = yearCashItem == null ? BigDecimal.ZERO + : NumberUtils.zeroIfNull(yearCashItem.getClosingAmount()); + BigDecimal currentAmount = NumberUtils.zeroIfNull(otherOperating.getCurrentAmount()) + .add(NumberUtils.zeroIfNull(endingCash.getCurrentAmount())).subtract(currentCash); + BigDecimal yearAmount = NumberUtils.zeroIfNull(otherOperating.getYearAmount()) + .add(NumberUtils.zeroIfNull(endingCash.getYearAmount())).subtract(yearCash); + // 2. 倒挤差额为正时,覆盖其他经营活动的原计算值 + FmsCashFlowStatementReportDO otherOperatingReport = reportMap.get(OTHER_OPERATING_ROW_NO); + if (currentAmount.signum() > 0 + && NumberUtils.zeroIfNull(otherOperatingReport.getCurrentAmount()).signum() == 0) { + otherOperating.setCurrentAmount(currentAmount); + } + if (yearAmount.signum() > 0 + && NumberUtils.zeroIfNull(otherOperatingReport.getYearAmount()).signum() == 0) { + otherOperating.setYearAmount(yearAmount); + } + // 3. 重算其他经营活动之后的行次公式项目,使净额和余额联动更新 + for (FmsReportItemRespVO item : result) { + if (item.getRowNo() == null || item.getRowNo() <= OTHER_OPERATING_ROW_NO + || !reportCommonService.isLineFormula(item.getFormula())) { + continue; + } + item.setCurrentAmount(calculateCashFlowFormula(item.getFormula(), currentBalanceMap, + Collections.emptyMap(), adjustmentMap, lineMap, true)) + .setYearAmount(calculateCashFlowFormula(item.getFormula(), yearBalanceMap, + Collections.emptyMap(), adjustmentMap, lineMap, false)); + lineMap.put(item.getRowNo(), item); + } + } + + /** + * 获得账套的现金流量表配置,首次查询时按报表模板初始化 + * + * @param accountSetId 账套编号 + * @return 报表配置列表 + */ + private List getOrInitCashFlowConfigs(Long accountSetId) { + // 1. 已初始化时直接返回 + List configs = cashFlowStatementConfigMapper + .selectListByAccountSetId(accountSetId); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 2. 锁定账套后二次查询,避免并发首次查询重复初始化 + accountSetService.lockAccountSet(accountSetId); + configs = cashFlowStatementConfigMapper.selectListByAccountSetId(accountSetId); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 3. 按报表模板初始化,模板为空时返回空列表 + List templates = reportTemplateMapper.selectListByType( + FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()); + if (CollUtil.isEmpty(templates)) { + return Collections.emptyList(); + } + // 4. 现金流量表公式使用跨报表表达式,不涉及科目绑定,直接复制模板配置 + configs = convertList(templates, template -> BeanUtils.toBean(template, + FmsCashFlowStatementConfigDO.class).setId(null).setAccountSetId(accountSetId)); + cashFlowStatementConfigMapper.insertBatch(configs); + return configs; + } + + /** + * 获得查询期间的报表快照,首次查询该期间时按当前配置创建 + * + * @param listReqVO 查询条件 + * @param configs 报表配置列表 + * @return 报表快照列表 + */ + private List getOrCreateCashFlowReports( + FmsReportListReqVO listReqVO, List configs) { + // 1. 已生成快照时直接返回;配置为空时不生成 + Integer fromPeriod = reportCommonService.parsePeriod(listReqVO.getStartMonth()); + Integer toPeriod = reportCommonService.parsePeriod(listReqVO.getEndMonth()); + Integer periodType = reportCommonService.getPeriodType(fromPeriod, toPeriod); + List reports = cashFlowStatementReportMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType); + if (CollUtil.isNotEmpty(reports) || CollUtil.isEmpty(configs)) { + return reports; + } + // 2. 锁定账套后二次查询,避免并发首次查询重复生成期间快照 + accountSetService.lockAccountSet(listReqVO.getAccountSetId()); + reports = cashFlowStatementReportMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType); + if (CollUtil.isNotEmpty(reports)) { + return reports; + } + // 3. 按当前配置创建期间快照,金额初始化为零 + reports = convertList(configs, config -> BeanUtils.toBean(config, FmsCashFlowStatementReportDO.class) + .setId(null).setFromPeriod(fromPeriod).setToPeriod(toPeriod).setType(periodType) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO)); + cashFlowStatementReportMapper.insertBatch(reports); + return reports; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementService.java new file mode 100644 index 0000000..5ea2b17 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementService.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.income.FmsIncomeStatementCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; + +import java.util.List; + +/** + * FMS 利润表 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsIncomeStatementService { + + /** + * 获得利润表 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 利润表 + */ + List getIncomeStatement(FmsReportListReqVO listReqVO, Long userId); + + /** + * 检查利润表与资产负债表的勾稽关系和公式覆盖状态 + * + * @param listReqVO 查询条件 + * @param userId 用户编号 + * @return 检查结果 + */ + FmsIncomeStatementCheckRespVO checkIncomeStatement(FmsReportListReqVO listReqVO, Long userId); + + /** + * 更新利润表公式 + * + * @param updateReqVO 公式信息 + * @param userId 用户编号 + */ + void updateIncomeStatementFormula(FmsReportFormulaUpdateReqVO updateReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementServiceImpl.java new file mode 100644 index 0000000..1ac5cbb --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementServiceImpl.java @@ -0,0 +1,290 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.income.FmsIncomeStatementCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.income.FmsIncomeStatementConfigDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.income.FmsIncomeStatementReportDO; +import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.income.FmsIncomeStatementConfigMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.income.FmsIncomeStatementReportMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_CONFIG_NOT_EXISTS; + +/** + * FMS 利润表 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsIncomeStatementServiceImpl implements FmsIncomeStatementService { + + /** + * 利润表净利润行次 + */ + private static final int NET_PROFIT_ROW_NO = 32; + /** + * 资产负债表未分配利润行次 + */ + private static final int RETAINED_EARNINGS_ROW_NO = 51; + /** + * 以前年度损益调整等科目编码前缀,不纳入利润表公式覆盖检查 + */ + private static final String EXCLUDED_PROFIT_LOSS_CODE_PREFIX = "6"; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + private FmsReportCommonService reportCommonService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsBalanceSheetService balanceSheetService; + @Resource + private FmsReportTemplateMapper reportTemplateMapper; + @Resource + private FmsIncomeStatementConfigMapper incomeStatementConfigMapper; + @Resource + private FmsIncomeStatementReportMapper incomeStatementReportMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public List getIncomeStatement(FmsReportListReqVO listReqVO, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(listReqVO.getAccountSetId(), userId); + // 1.2 加载科目并初始化报表配置 + List subjects = subjectService.getSubjectList(listReqVO.getAccountSetId(), null, userId); + List configs = getOrInitIncomeConfigs(listReqVO.getAccountSetId(), subjects); + List reports = getOrCreateIncomeReports(listReqVO, configs); + + // 2. 查询本期和本年科目发生额 + Map balanceMap = reportCommonService.getSubjectBalanceMap( + listReqVO.getAccountSetId(), LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth()), + LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth()), userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + + // 3. 计算报表项目 + Map lineMap = new LinkedHashMap<>(); + List result = new ArrayList<>(); + for (FmsIncomeStatementReportDO report : reports) { + FmsIncomeStatementConfigDO config = BeanUtils.toBean(report, FmsIncomeStatementConfigDO.class); + FmsReportItemRespVO item = buildIncomeItem(config, subjectMap, balanceMap, lineMap); + if (config.getRowNo() != null && config.getRowNo() > 0) { + lineMap.put(config.getRowNo(), item); + } + result.add(item); + } + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FmsIncomeStatementCheckRespVO checkIncomeStatement(FmsReportListReqVO listReqVO, Long userId) { + // 1. 检查利润表与资产负债表的勾稽关系:净利润本年累计应等于未分配利润变动 + List incomeItems = getIncomeStatement(listReqVO, userId); + FmsReportItemRespVO netProfitItem = CollUtil.findOne(incomeItems, + item -> ObjUtil.equal(item.getRowNo(), NET_PROFIT_ROW_NO)); + List balanceRows = balanceSheetService.getBalanceSheet(listReqVO, userId); + FmsBalanceSheetRowRespVO retainedEarningsRow = CollUtil.findOne(balanceRows, + row -> ObjUtil.equal(row.getLiabilityRowNo(), RETAINED_EARNINGS_ROW_NO)); + BigDecimal retainedEarningsChange = NumberUtils.zeroIfNull(retainedEarningsRow == null + ? null : retainedEarningsRow.getLiabilityClosingAmount()).subtract(NumberUtils.zeroIfNull( + retainedEarningsRow == null ? null : retainedEarningsRow.getLiabilityOpeningAmount())); + BigDecimal differenceAmount = NumberUtils.zeroIfNull( + netProfitItem == null ? null : netProfitItem.getYearAmount()).subtract(retainedEarningsChange); + + // 2. 检查未纳入利润表公式的一级损益类科目 + List subjects = subjectService.getSubjectList(listReqVO.getAccountSetId(), null, userId); + Set formulaSubjectIds = new LinkedHashSet<>(); + for (FmsReportItemRespVO item : incomeItems) { + formulaSubjectIds.addAll(reportCommonService.parseFormulaSubjectIds(item.getFormula())); + } + List unmappedSubjects = new ArrayList<>(); + for (FmsSubjectDO subject : subjects) { + // 一级损益类科目未纳入公式时需要提示补充;以前年度损益调整等 6 开头科目除外 + if (!reportCommonService.isRootSubject(subject) + || ObjUtil.notEqual(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + || StrUtil.startWith(subject.getCode(), EXCLUDED_PROFIT_LOSS_CODE_PREFIX) + || reportCommonService.isSubjectTreeMapped(subject, subjects, formulaSubjectIds)) { + continue; + } + unmappedSubjects.add(new FmsIncomeStatementCheckRespVO.UnmappedSubject() + .setId(subject.getId()).setCode(subject.getCode()).setName(subject.getName())); + } + return new FmsIncomeStatementCheckRespVO().setBalanced(differenceAmount.signum() == 0) + .setDifferenceAmount(differenceAmount).setUnmappedSubjects(unmappedSubjects); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SuppressWarnings("DataFlowIssue") + public void updateIncomeStatementFormula(FmsReportFormulaUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验报表项目 + FmsIncomeStatementReportDO report = incomeStatementReportMapper.selectById(updateReqVO.getId()); + reportCommonService.validateReportItemEditable(updateReqVO.getAccountSetId(), + report == null ? null : report.getAccountSetId(), report == null ? null : report.getEditable()); + // 2. 校验并构造利润表公式 + List formulas = reportCommonService.buildReportFormulaList(updateReqVO, + FmsFormulaRuleEnum.DEBIT_AMOUNT.getRule(), FmsFormulaRuleEnum.PROFIT_LOSS_AMOUNT.getRule(), userId); + + // 3. 只更新当前期间报表快照,后续期间仍使用基础配置 + incomeStatementReportMapper.updateById(new FmsIncomeStatementReportDO().setId(report.getId()) + .setFormula(JsonUtils.toJsonString(formulas))); + } + + /** + * 计算报表项目的本期和本年累计金额 + * + * 行次公式直接引用已计算的行次金额;科目公式逐项取数并按运算符累加,取数结果回写公式项用于前端展示 + * + * @param config 报表配置 + * @param subjectMap 科目编号到科目的 Map + * @param balanceMap 科目编号到余额的 Map + * @param lineMap 行次到已计算报表项目的 Map + * @return 报表项目 + */ + private FmsReportItemRespVO buildIncomeItem(FmsIncomeStatementConfigDO config, + Map subjectMap, Map balanceMap, + Map lineMap) { + FmsReportItemRespVO item = new FmsReportItemRespVO().setId(config.getId()).setName(config.getName()) + .setRowNo(config.getRowNo()).setLevel(config.getLevel()).setEditable(config.getEditable()) + .setFormula(config.getFormula()).setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO); + // 行次公式直接引用前面已计算的行次金额,例如 ["L1-L2"] + if (reportCommonService.isLineFormula(config.getFormula())) { + item.setCurrentAmount(reportCommonService.calculateItemLineFormula( + config.getFormula(), lineMap, false, true)) + .setYearAmount(reportCommonService.calculateItemLineFormula( + config.getFormula(), lineMap, false, false)); + return item; + } + // 科目公式逐项取数,按运算符累加到本期、本年累计金额 + List formulas = reportCommonService.parseSubjectFormula(config.getFormula()); + for (FmsReportFormulaRespVO formula : formulas) { + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(formula.getSubjectId()); + FmsSubjectDO subject = subjectMap.get(formula.getSubjectId()); + reportCommonService.normalizeSubjectFormula(formula, subject); + BigDecimal currentAmount = reportCommonService.calculateIncomeOccurrenceAmount( + formula.getRules(), subject, balance, true, formula.getOperator()); + BigDecimal yearAmount = reportCommonService.calculateIncomeOccurrenceAmount( + formula.getRules(), subject, balance, false, formula.getOperator()); + formula.setCurrentAmount(currentAmount).setYearAmount(yearAmount); + item.setCurrentAmount(reportCommonService.applyOperator( + item.getCurrentAmount(), currentAmount, formula.getOperator())) + .setYearAmount(reportCommonService.applyOperator( + item.getYearAmount(), yearAmount, formula.getOperator())); + } + // 回写含取数结果的公式,供前端公式编辑弹窗展示各项金额 + item.setFormula(JsonUtils.toJsonString(formulas)); + return item; + } + + /** + * 获得账套的报表配置,首次查询时按报表模板初始化 + * + * 初始化时将模板公式中的科目编码绑定为账套下的科目编号,编码不存在的科目编号置空 + * + * @param accountSetId 账套编号 + * @param subjects 账套下的科目列表 + * @return 报表配置列表 + */ + private List getOrInitIncomeConfigs( + Long accountSetId, List subjects) { + // 1. 已初始化时直接返回 + List configs = incomeStatementConfigMapper.selectListByAccountSetId(accountSetId); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 2. 锁定账套后二次查询,避免并发首次查询重复初始化 + accountSetService.lockAccountSet(accountSetId); + configs = incomeStatementConfigMapper.selectListByAccountSetId(accountSetId); + if (CollUtil.isNotEmpty(configs)) { + return configs; + } + // 3. 按报表模板初始化,模板为空时返回空列表 + List templates = reportTemplateMapper.selectListByType( + FmsReportTypeEnum.INCOME_STATEMENT.getType()); + if (CollUtil.isEmpty(templates)) { + return Collections.emptyList(); + } + // 4. 模板公式中的科目编码绑定为账套下的科目编号后批量插入 + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode, + Function.identity(), (first, second) -> first); + configs = convertList(templates, template -> BeanUtils.toBean(template, FmsIncomeStatementConfigDO.class) + .setId(null).setAccountSetId(accountSetId) + .setFormula(reportCommonService.bindSubjectFormula(template.getFormula(), subjectMap))); + incomeStatementConfigMapper.insertBatch(configs); + return configs; + } + + /** + * 获得查询期间的报表快照,首次查询该期间时按当前配置创建 + * + * @param listReqVO 查询条件 + * @param configs 报表配置列表 + * @return 报表快照列表 + */ + private List getOrCreateIncomeReports( + FmsReportListReqVO listReqVO, List configs) { + // 1. 已生成快照时直接返回;配置为空时不生成 + Integer fromPeriod = reportCommonService.parsePeriod(listReqVO.getStartMonth()); + Integer toPeriod = reportCommonService.parsePeriod(listReqVO.getEndMonth()); + Integer periodType = reportCommonService.getPeriodType(fromPeriod, toPeriod); + List reports = incomeStatementReportMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType); + if (CollUtil.isNotEmpty(reports) || CollUtil.isEmpty(configs)) { + return reports; + } + // 2. 锁定账套后二次查询,避免并发首次查询重复生成期间快照 + accountSetService.lockAccountSet(listReqVO.getAccountSetId()); + reports = incomeStatementReportMapper.selectListByPeriod( + listReqVO.getAccountSetId(), fromPeriod, toPeriod, periodType); + if (CollUtil.isNotEmpty(reports)) { + return reports; + } + // 3. 按当前配置创建期间快照,金额初始化为零、未结账 + reports = convertList(configs, config -> BeanUtils.toBean(config, FmsIncomeStatementReportDO.class) + .setId(null).setFromPeriod(fromPeriod).setToPeriod(toPeriod).setType(periodType) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO).setSettled(false)); + incomeStatementReportMapper.insertBatch(reports); + return reports; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonService.java new file mode 100644 index 0000000..d1a85fb --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonService.java @@ -0,0 +1,203 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; + +import java.math.BigDecimal; +import java.time.YearMonth; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * FMS 报表共用 Service 接口 + * + * 负责资产负债、利润和现金流量三大报表共用的账簿余额查询、报表公式解析计算和公式覆盖检查 + * + * @author 芋道源码 + */ +public interface FmsReportCommonService { + + /** + * 获得指定月份区间的科目余额 Map,树形余额扁平化后按科目编号唯一存放 + * + * @param accountSetId 账套编号 + * @param startMonth 开始月份 + * @param endMonth 结束月份 + * @param userId 用户编号 + * @return 科目编号到余额的 Map + */ + Map getSubjectBalanceMap(Long accountSetId, + YearMonth startMonth, YearMonth endMonth, Long userId); + + /** + * 校验并构造报表公式项,公式项中的科目必须存在且不重复 + * + * @param updateReqVO 公式更新请求 + * @param minimumRule 允许的最小取数规则 + * @param maximumRule 允许的最大取数规则 + * @param userId 用户编号 + * @return 报表公式项 + */ + List buildReportFormulaList(FmsReportFormulaUpdateReqVO updateReqVO, + Integer minimumRule, Integer maximumRule, Long userId); + + /** + * 校验报表项目属于当前账套且允许编辑公式 + * + * @param accountSetId 账套编号 + * @param itemAccountSetId 报表项目的账套编号 + * @param editable 报表项目是否可编辑 + */ + void validateReportItemEditable(Long accountSetId, Long itemAccountSetId, Boolean editable); + + /** + * 将模板公式中的科目编码绑定为账套下的科目编号和名称,编码不存在时科目编号置空 + * + * @param formula 模板公式 + * @param subjectMap 科目编码到科目的 Map + * @return 绑定后的公式 + */ + String bindSubjectFormula(String formula, Map subjectMap); + + /** + * 解析科目公式,空公式返回空列表 + * + * @param formula 公式 + * @return 公式项列表 + */ + List parseSubjectFormula(String formula); + + /** + * 解析科目公式中的科目编号集合,空公式和行次公式返回空集合 + * + * @param formula 公式 + * @return 科目编号集合 + */ + Set parseFormulaSubjectIds(String formula); + + /** + * 判断一级科目自身或下级科目是否已纳入报表公式 + * + * @param rootSubject 一级科目 + * @param subjects 科目列表 + * @param formulaSubjectIds 公式中的科目编号集合 + * @return 是否已纳入报表公式 + */ + boolean isSubjectTreeMapped(FmsSubjectDO rootSubject, List subjects, + Set formulaSubjectIds); + + /** + * 判断是否为一级科目 + * + * @param subject 科目 + * @return 是否一级科目 + */ + boolean isRootSubject(FmsSubjectDO subject); + + /** + * 判断是否为行次公式,例如 ["L1-L2"] + * + * @param formula 公式 + * @return 是否行次公式 + */ + boolean isLineFormula(String formula); + + /** + * 按运算符累加或扣减金额 + * + * @param result 累计结果 + * @param amount 金额 + * @param operator 运算符 + * @return 计算结果 + */ + BigDecimal applyOperator(BigDecimal result, BigDecimal amount, String operator); + + /** + * 将公式项的科目名称和编码刷新为科目当前值 + * + * @param formula 公式项 + * @param subject 科目 + */ + void normalizeSubjectFormula(FmsReportFormulaRespVO formula, FmsSubjectDO subject); + + /** + * 计算余额类取数规则的金额,按科目余额方向决定借贷相减的方向 + * + * @param rule 取数规则 + * @param subject 科目 + * @param balanceMap 科目编号到余额的 Map + * @param subjectMap 科目编号到科目的 Map + * @param opening 是否取期初余额,否则取期末余额 + * @return 金额 + */ + BigDecimal calculateBalanceAmount(Integer rule, FmsSubjectDO subject, + Map balanceMap, + Map subjectMap, boolean opening); + + /** + * 计算发生额类取数规则的金额,损益发生额按科目余额方向决定借贷相减的方向 + * + * @param rule 取数规则 + * @param subject 科目 + * @param balance 科目余额 + * @param current 是否取本期发生额,否则取本年累计发生额 + * @return 金额 + */ + BigDecimal calculateOccurrenceAmount(Integer rule, FmsSubjectDO subject, + FmsLedgerSubjectBalanceRespVO balance, boolean current); + + /** + * 计算利润表发生额,正向公式项按既有业务语义抵减反方向发生额 + * + * @param rule 取数规则 + * @param subject 科目 + * @param balance 科目余额 + * @param current 是否取本期发生额,否则取本年累计发生额 + * @param operator 运算符 + * @return 金额 + */ + BigDecimal calculateIncomeOccurrenceAmount(Integer rule, FmsSubjectDO subject, + FmsLedgerSubjectBalanceRespVO balance, boolean current, String operator); + + /** + * 计算报表项目的行次公式金额,例如 ["L1-L2"] + * + * @param formula 行次公式 + * @param lineMap 行次到报表项目的 Map + * @param opening 是否取期初金额 + * @param current 非期初时是否取本期金额,否则取本年累计或期末金额 + * @return 金额 + */ + BigDecimal calculateItemLineFormula(String formula, Map lineMap, + boolean opening, boolean current); + + /** + * 解析行次公式表达式数组 + * + * @param formula 行次公式 + * @return 表达式列表 + */ + List parseLineFormula(String formula); + + /** + * 解析会计期间,格式为 yyyyMM + * + * @param month 月份,格式为 yyyy-MM + * @return 会计期间 + */ + Integer parsePeriod(String month); + + /** + * 获得报表期间类型,开始和结束期间相同为月度,否则为季度 + * + * @param fromPeriod 开始会计期间 + * @param toPeriod 结束会计期间 + * @return 报表期间类型 + */ + Integer getPeriodType(Integer fromPeriod, Integer toPeriod); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonServiceImpl.java new file mode 100644 index 0000000..92fb620 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonServiceImpl.java @@ -0,0 +1,504 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSubjectAmountVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsReportPeriodTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.LEDGER_PERIOD_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_CONFIG_NOT_EDITABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_CONFIG_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_FORMULA_OPERATOR_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_FORMULA_RULE_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_FORMULA_SUBJECT_DUPLICATE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_NOT_EXISTS; + +/** + * FMS 报表共用 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsReportCommonServiceImpl implements FmsReportCommonService { + + private static final DateTimeFormatter MONTH_FORMATTER = DatePattern.NORM_MONTH_FORMATTER; + private static final Pattern LINE_FORMULA_PATTERN = Pattern.compile("([+-]?)(L\\d+)"); + + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsLedgerService ledgerService; + @Resource + @Lazy // 避免循环依赖 + private FmsClosingSchemeService closingSchemeService; + @Resource + @Lazy // 避免循环依赖 + private FmsClosingVoucherService closingVoucherService; + @Resource + @Lazy // 避免循环依赖 + private FmsVoucherService voucherService; + + @Override + public Map getSubjectBalanceMap(Long accountSetId, + YearMonth startMonth, YearMonth endMonth, Long userId) { + // 1. 查询普通账簿科目余额 + FmsLedgerListReqVO listReqVO = new FmsLedgerListReqVO().setAccountSetId(accountSetId) + .setStartMonth(startMonth.format(MONTH_FORMATTER)) + .setEndMonth(endMonth.format(MONTH_FORMATTER)); + List balances = flattenBalances(ledgerService.getSubjectBalanceList(listReqVO, userId)); + Map balanceMap = convertMap( + balances, FmsLedgerSubjectBalanceRespVO::getSubjectId, Function.identity(), + (first, second) -> first, LinkedHashMap::new); + + // 2. 查询结转损益凭证在期初、本期和本年的科目发生额 + LocalDateTime startTime = LocalDateTimeUtils.getMonthBeginTime(startMonth); + LocalDateTime endTime = LocalDateTimeUtils.getNextMonthBeginTime(endMonth); + LocalDateTime yearBeginTime = LocalDateTimeUtils.getMonthBeginTime(endMonth.getYear(), 1); + FmsClosingSchemeDO profitLossClosing = closingSchemeService.getClosingSchemeByAccountSetIdAndType( + accountSetId, FmsClosingTypeEnum.PROFIT_LOSS.getType()); + if (profitLossClosing == null) { + return balanceMap; + } + List closingVouchers = closingVoucherService + .getClosingVoucherListByClosingIdAndPeriod(profitLossClosing.getId(), null, endTime); + List openingAmounts = getProfitLossClosingSubjectAmountList( + closingVouchers, null, startTime); + List periodAmounts = getProfitLossClosingSubjectAmountList( + closingVouchers, startTime, endTime); + List yearAmounts = getProfitLossClosingSubjectAmountList( + closingVouchers, yearBeginTime, endTime); + + // 3. 从损益类科目及其父级余额中扣除结转损益凭证发生额 + Map subjectMap = convertMap( + subjectService.getSubjectList(accountSetId, null, userId), FmsSubjectDO::getId); + adjustProfitLossClosingAmounts(balanceMap, subjectMap, openingAmounts, periodAmounts, yearAmounts); + return balanceMap; + } + + /** + * 获得指定期间内损益结转凭证的科目发生额 + * + * @param closingVouchers 损益结转凭证关联列表 + * @param beginTime 开始时间,为空时不限制 + * @param endTime 结束时间 + * @return 科目发生额列表 + */ + private List getProfitLossClosingSubjectAmountList( + List closingVouchers, LocalDateTime beginTime, LocalDateTime endTime) { + // 1. 按期间筛选损益结转凭证编号 + List periodClosingVouchers = filterList(closingVouchers, + item -> (beginTime == null || !item.getVoucherTime().isBefore(beginTime)) + && item.getVoucherTime().isBefore(endTime)); + // 2. 按关联凭证编号聚合科目发生额 + return voucherService.getVoucherSubjectAmountList( + convertList(periodClosingVouchers, FmsClosingVoucherDO::getVoucherId)); + } + + /** + * 从报表余额中扣除结转损益凭证发生额 + * + * 只处理损益类科目,并同步调整其父级汇总;普通结转凭证和非损益结转科目保持原余额 + * + * @param balanceMap 科目余额 Map + * @param subjectMap 科目 Map + * @param openingAmounts 期初前发生额 + * @param periodAmounts 本期发生额 + * @param yearAmounts 本年发生额 + */ + private void adjustProfitLossClosingAmounts(Map balanceMap, Map subjectMap, + List openingAmounts, List periodAmounts, + List yearAmounts) { + // 1. 按科目编号建立各期间结转损益发生额索引 + Map openingAmountMap = convertMap(openingAmounts, FmsVoucherSubjectAmountVO::getSubjectId); + Map periodAmountMap = convertMap(periodAmounts, FmsVoucherSubjectAmountVO::getSubjectId); + Map yearAmountMap = convertMap(yearAmounts, FmsVoucherSubjectAmountVO::getSubjectId); + // 2. 从损益科目及其父级汇总余额中逐级扣除结转发生额 + for (FmsSubjectDO subject : subjectMap.values()) { + if (ObjUtil.notEqual(subject.getType(), FmsSubjectTypeEnum.PROFIT_LOSS.getType())) { + continue; + } + FmsVoucherSubjectAmountVO openingAmount = openingAmountMap.get(subject.getId()); + FmsVoucherSubjectAmountVO periodAmount = periodAmountMap.get(subject.getId()); + FmsVoucherSubjectAmountVO yearAmount = yearAmountMap.get(subject.getId()); + if (openingAmount == null && periodAmount == null && yearAmount == null) { + continue; + } + FmsSubjectDO currentSubject = subject; + for (int level = 0; currentSubject != null && level < subjectMap.size(); level++) { + adjustSubjectBalance(balanceMap.get(currentSubject.getId()), openingAmount, periodAmount, yearAmount); + currentSubject = subjectMap.get(currentSubject.getParentId()); + } + } + } + + /** + * 扣除单个损益科目发生额 + * + * @param balance 科目余额 + * @param openingAmount 期初前发生额 + * @param periodAmount 本期发生额 + * @param yearAmount 本年发生额 + */ + private void adjustSubjectBalance(FmsLedgerSubjectBalanceRespVO balance, + FmsVoucherSubjectAmountVO openingAmount, FmsVoucherSubjectAmountVO periodAmount, + FmsVoucherSubjectAmountVO yearAmount) { + // 1. 账簿中不存在的科目无需调整 + if (balance == null) { + return; + } + // 2. 扣除结转损益凭证,重新计算期初和期末净余额 + BigDecimal openingDebitAmount = getDebitAmount(openingAmount); + BigDecimal openingCreditAmount = getCreditAmount(openingAmount); + BigDecimal periodDebitAmount = getDebitAmount(periodAmount); + BigDecimal periodCreditAmount = getCreditAmount(periodAmount); + BigDecimal openingBalance = NumberUtils.zeroIfNull(balance.getOpeningDebitAmount()) + .subtract(NumberUtils.zeroIfNull(balance.getOpeningCreditAmount())) + .subtract(openingDebitAmount).add(openingCreditAmount); + BigDecimal endingBalance = NumberUtils.zeroIfNull(balance.getEndingDebitAmount()) + .subtract(NumberUtils.zeroIfNull(balance.getEndingCreditAmount())) + .subtract(openingDebitAmount).add(openingCreditAmount) + .subtract(periodDebitAmount).add(periodCreditAmount); + // 3. 回写期初、本期、本年和期末借贷金额 + balance.setOpeningDebitAmount(openingBalance.max(BigDecimal.ZERO)) + .setOpeningCreditAmount(openingBalance.min(BigDecimal.ZERO).abs()) + .setPeriodDebitAmount(NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()).subtract(periodDebitAmount)) + .setPeriodCreditAmount(NumberUtils.zeroIfNull(balance.getPeriodCreditAmount()).subtract(periodCreditAmount)) + .setYearDebitAmount(NumberUtils.zeroIfNull(balance.getYearDebitAmount()).subtract(getDebitAmount(yearAmount))) + .setYearCreditAmount(NumberUtils.zeroIfNull(balance.getYearCreditAmount()).subtract(getCreditAmount(yearAmount))) + .setEndingDebitAmount(endingBalance.max(BigDecimal.ZERO)) + .setEndingCreditAmount(endingBalance.min(BigDecimal.ZERO).abs()); + } + + private BigDecimal getDebitAmount(FmsVoucherSubjectAmountVO amount) { + return amount == null ? BigDecimal.ZERO : NumberUtils.zeroIfNull(amount.getDebitAmount()); + } + + private BigDecimal getCreditAmount(FmsVoucherSubjectAmountVO amount) { + return amount == null ? BigDecimal.ZERO : NumberUtils.zeroIfNull(amount.getCreditAmount()); + } + + @Override + public List buildReportFormulaList(FmsReportFormulaUpdateReqVO updateReqVO, + Integer minimumRule, Integer maximumRule, Long userId) { + // 1. 校验科目不重复 + List formulaReqVOs = updateReqVO.getFormulas(); + List subjectIds = convertList(formulaReqVOs, FmsReportFormulaUpdateReqVO.Formula::getSubjectId); + if (new LinkedHashSet<>(subjectIds).size() != subjectIds.size()) { + throw exception(REPORT_FORMULA_SUBJECT_DUPLICATE); + } + Map subjectMap = convertMap( + subjectService.getSubjectList(updateReqVO.getAccountSetId(), null, userId), FmsSubjectDO::getId); + // 2. 校验运算符、取数规则和科目存在性,构造公式项 + List formulas = new ArrayList<>(); + for (FmsReportFormulaUpdateReqVO.Formula formulaReqVO : formulaReqVOs) { + if (!StrUtil.equalsAny(formulaReqVO.getOperator(), "+", "-")) { + throw exception(REPORT_FORMULA_OPERATOR_INVALID); + } + if (formulaReqVO.getRules() == null || formulaReqVO.getRules() < minimumRule + || formulaReqVO.getRules() > maximumRule) { + throw exception(REPORT_FORMULA_RULE_INVALID); + } + FmsSubjectDO subject = subjectMap.get(formulaReqVO.getSubjectId()); + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + formulas.add(new FmsReportFormulaRespVO().setSubjectId(subject.getId()) + .setSubjectName(subject.getName()).setSubjectNumber(subject.getCode()) + .setOperator(formulaReqVO.getOperator()).setRules(formulaReqVO.getRules())); + } + return formulas; + } + + @Override + public void validateReportItemEditable(Long accountSetId, Long itemAccountSetId, Boolean editable) { + if (itemAccountSetId == null || ObjUtil.notEqual(accountSetId, itemAccountSetId)) { + throw exception(REPORT_CONFIG_NOT_EXISTS); + } + if (Boolean.FALSE.equals(editable)) { + throw exception(REPORT_CONFIG_NOT_EDITABLE); + } + } + + @Override + public String bindSubjectFormula(String formula, Map subjectMap) { + // 空公式和行次公式不含科目编码,直接返回原公式 + if (StrUtil.isEmpty(formula) || isLineFormula(formula)) { + return formula; + } + // 按科目编码绑定账套下的科目编号和名称,编码不存在时科目编号置空,由前端提示科目已失效 + List formulas = parseSubjectFormula(formula); + formulas.forEach(item -> { + FmsSubjectDO subject = subjectMap.get(item.getSubjectNumber()); + if (subject != null) { + item.setSubjectId(subject.getId()).setSubjectName(subject.getName()).setSubjectNumber(subject.getCode()); + } else { + item.setSubjectId(null); + } + }); + return JsonUtils.toJsonString(formulas); + } + + @Override + public List parseSubjectFormula(String formula) { + if (StrUtil.isEmpty(formula)) { + return new ArrayList<>(); + } + return JsonUtils.parseArray(formula, FmsReportFormulaRespVO.class); + } + + @Override + public Set parseFormulaSubjectIds(String formula) { + Set subjectIds = new HashSet<>(); + if (StrUtil.isEmpty(formula) || !formula.contains("{")) { + return subjectIds; + } + for (FmsReportFormulaRespVO formulaItem : parseSubjectFormula(formula)) { + if (formulaItem.getSubjectId() != null) { + subjectIds.add(formulaItem.getSubjectId()); + } + } + return subjectIds; + } + + @Override + public boolean isSubjectTreeMapped(FmsSubjectDO rootSubject, List subjects, Set formulaSubjectIds) { + // 一级科目自身已纳入公式时直接命中,否则沿公式科目的父级链向上查找 + if (formulaSubjectIds.contains(rootSubject.getId())) { + return true; + } + // 逐个公式科目沿父级链向上查找,任一祖先为当前一级科目即视为已纳入 + Map subjectMap = convertMap(subjects, FmsSubjectDO::getId); + for (Long subjectId : formulaSubjectIds) { + FmsSubjectDO currentSubject = subjectMap.get(subjectId); + while (currentSubject != null && currentSubject.getParentId() != null + && currentSubject.getParentId() > 0) { + if (ObjUtil.equal(currentSubject.getParentId(), rootSubject.getId())) { + return true; + } + currentSubject = subjectMap.get(currentSubject.getParentId()); + } + } + return false; + } + + @Override + public boolean isRootSubject(FmsSubjectDO subject) { + return subject.getParentId() == null || subject.getParentId() == 0; + } + + @Override + public boolean isLineFormula(String formula) { + return StrUtil.isNotEmpty(formula) && formula.contains("L"); + } + + @Override + public BigDecimal applyOperator(BigDecimal result, BigDecimal amount, String operator) { + return StrUtil.equals(operator, "-") ? result.subtract(amount) : result.add(amount); + } + + @Override + public void normalizeSubjectFormula(FmsReportFormulaRespVO formula, FmsSubjectDO subject) { + if (subject != null) { + formula.setSubjectName(subject.getName()).setSubjectNumber(subject.getCode()); + } + } + + @Override + public BigDecimal calculateBalanceAmount(Integer rule, FmsSubjectDO subject, + Map balanceMap, + Map subjectMap, boolean opening) { + // 科目缺失时按零处理 + if (subject == null) { + return BigDecimal.ZERO; + } + // 科目借/贷方余额按一级子科目分别判断后求和,避免父科目先净额抵销 + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.SUBJECT_DEBIT_BALANCE.getRule()) + || ObjUtil.equal(rule, FmsFormulaRuleEnum.SUBJECT_CREDIT_BALANCE.getRule())) { + List childSubjectIds = convertList(filterList(subjectMap.values(), + item -> ObjUtil.equal(item.getParentId(), subject.getId())), FmsSubjectDO::getId); + if (CollUtil.isEmpty(childSubjectIds)) { + childSubjectIds = Collections.singletonList(subject.getId()); + } + boolean debitRule = ObjUtil.equal(rule, FmsFormulaRuleEnum.SUBJECT_DEBIT_BALANCE.getRule()); + BigDecimal amount = BigDecimal.ZERO; + for (Long childSubjectId : childSubjectIds) { + FmsLedgerSubjectBalanceRespVO childBalance = balanceMap.get(childSubjectId); + if (childBalance == null) { + continue; + } + BigDecimal childAmount = opening + ? (debitRule ? childBalance.getOpeningDebitAmount() : childBalance.getOpeningCreditAmount()) + : (debitRule ? childBalance.getEndingDebitAmount() : childBalance.getEndingCreditAmount()); + amount = amount.add(NumberUtils.zeroIfNull(childAmount)); + } + return amount; + } + FmsLedgerSubjectBalanceRespVO balance = balanceMap.get(subject.getId()); + if (balance == null) { + return BigDecimal.ZERO; + } + // 余额规则按科目余额方向决定借贷相减方向,借方、贷方余额规则直接取对应方向金额 + BigDecimal debitAmount = opening ? NumberUtils.zeroIfNull(balance.getOpeningDebitAmount()) + : NumberUtils.zeroIfNull(balance.getEndingDebitAmount()); + BigDecimal creditAmount = opening ? NumberUtils.zeroIfNull(balance.getOpeningCreditAmount()) + : NumberUtils.zeroIfNull(balance.getEndingCreditAmount()); + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.BALANCE.getRule())) { + return ObjUtil.equal(subject.getBalanceDirection(), 1) + ? debitAmount.subtract(creditAmount) : creditAmount.subtract(debitAmount); + } + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.DEBIT_BALANCE.getRule())) { + return debitAmount; + } + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.CREDIT_BALANCE.getRule())) { + return creditAmount; + } + return BigDecimal.ZERO; + } + + @Override + public BigDecimal calculateOccurrenceAmount(Integer rule, FmsSubjectDO subject, + FmsLedgerSubjectBalanceRespVO balance, boolean current) { + // 余额或科目缺失时按零处理;按取数规则选择本期或本年累计的借贷方向金额 + if (balance == null || subject == null) { + return BigDecimal.ZERO; + } + // 损益发生额按科目余额方向决定借贷相减方向,借方、贷方发生额规则直接取对应方向金额 + BigDecimal debitAmount = current ? NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()) + : NumberUtils.zeroIfNull(balance.getYearDebitAmount()); + BigDecimal creditAmount = current ? NumberUtils.zeroIfNull(balance.getPeriodCreditAmount()) + : NumberUtils.zeroIfNull(balance.getYearCreditAmount()); + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.DEBIT_AMOUNT.getRule())) { + return debitAmount; + } + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.CREDIT_AMOUNT.getRule())) { + return creditAmount; + } + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.PROFIT_LOSS_AMOUNT.getRule())) { + return ObjUtil.equal(subject.getBalanceDirection(), 1) + ? debitAmount.subtract(creditAmount) : creditAmount.subtract(debitAmount); + } + return BigDecimal.ZERO; + } + + @Override + public BigDecimal calculateIncomeOccurrenceAmount(Integer rule, FmsSubjectDO subject, + FmsLedgerSubjectBalanceRespVO balance, boolean current, String operator) { + BigDecimal amount = calculateOccurrenceAmount(rule, subject, balance, current); + if (subject == null || balance == null || !StrUtil.equals(operator, "+")) { + return amount; + } + BigDecimal debitAmount = current ? NumberUtils.zeroIfNull(balance.getPeriodDebitAmount()) + : NumberUtils.zeroIfNull(balance.getYearDebitAmount()); + BigDecimal creditAmount = current ? NumberUtils.zeroIfNull(balance.getPeriodCreditAmount()) + : NumberUtils.zeroIfNull(balance.getYearCreditAmount()); + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.DEBIT_AMOUNT.getRule())) { + return debitAmount.subtract(creditAmount); + } + if (ObjUtil.equal(rule, FmsFormulaRuleEnum.CREDIT_AMOUNT.getRule())) { + return creditAmount.subtract(debitAmount); + } + return amount; + } + + @Override + public BigDecimal calculateItemLineFormula(String formula, Map lineMap, + boolean opening, boolean current) { + // 逐个匹配 L 行次引用,按运算符累加对应行次金额 + String expression = CollUtil.getFirst(parseLineFormula(formula)); + if (StrUtil.isEmpty(expression)) { + return BigDecimal.ZERO; + } + // 按 opening、current 选择行次的期初、本期、本年累计或期末金额,逐个累加 + BigDecimal result = BigDecimal.ZERO; + Matcher matcher = LINE_FORMULA_PATTERN.matcher(expression); + while (matcher.find()) { + Integer rowNo = Integer.valueOf(matcher.group(2).substring(1)); + FmsReportItemRespVO item = lineMap.get(rowNo); + BigDecimal amount = item == null ? BigDecimal.ZERO : opening + ? NumberUtils.zeroIfNull(item.getOpeningAmount()) + : current ? NumberUtils.zeroIfNull(item.getCurrentAmount()) + : item.getYearAmount() != null ? NumberUtils.zeroIfNull(item.getYearAmount()) + : NumberUtils.zeroIfNull(item.getClosingAmount()); + result = applyOperator(result, amount, matcher.group(1)); + } + return result; + } + + @Override + public List parseLineFormula(String formula) { + if (StrUtil.isEmpty(formula)) { + return new ArrayList<>(); + } + return JsonUtils.parseArray(formula, String.class); + } + + @Override + public Integer parsePeriod(String month) { + return Integer.valueOf(LocalDateTimeUtils.parseYearMonth(month).format(DatePattern.SIMPLE_MONTH_FORMATTER)); + } + + @Override + public Integer getPeriodType(Integer fromPeriod, Integer toPeriod) { + if (fromPeriod > toPeriod) { + throw exception(LEDGER_PERIOD_INVALID); + } + return ObjUtil.equal(fromPeriod, toPeriod) + ? FmsReportPeriodTypeEnum.MONTH.getType() : FmsReportPeriodTypeEnum.QUARTER.getType(); + } + + private List flattenBalances(List balances) { + List result = new ArrayList<>(); + for (FmsLedgerSubjectBalanceRespVO balance : balances) { + result.add(balance); + result.addAll(flattenBalances(balance.getChildren())); + } + return result; + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherService.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherService.java new file mode 100644 index 0000000..a58b815 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherService.java @@ -0,0 +1,253 @@ +package cn.iocoder.yudao.module.fms.service.voucher; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherAttachmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportTemplateVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherMoveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSubjectAmountVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherTidyReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; + +/** + * FMS 凭证 Service 接口 + * + * @author 芋道源码 + */ +public interface FmsVoucherService { + + /** + * 创建凭证 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 凭证编号 + */ + Long createVoucher(FmsVoucherSaveReqVO createReqVO, Long userId); + + /** + * 更新凭证 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateVoucher(FmsVoucherSaveReqVO updateReqVO, Long userId); + + /** + * 更新凭证附件 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateVoucherAttachments(FmsVoucherAttachmentUpdateReqVO updateReqVO, Long userId); + + /** + * 删除凭证 + * + * @param accountSetId 账套编号 + * @param ids 凭证编号数组 + * @param userId 用户编号 + */ + void deleteVoucherList(Long accountSetId, List ids, Long userId); + + /** + * 更新凭证审核状态 + * + * @param accountSetId 账套编号 + * @param ids 凭证编号数组 + * @param status 审核状态 + * @param userId 用户编号 + */ + void updateVoucherReviewStatus(Long accountSetId, List ids, Integer status, Long userId); + + /** + * 整理凭证号 + * + * @param tidyReqVO 整理信息 + * @param userId 用户编号 + */ + void tidyVoucher(FmsVoucherTidyReqVO tidyReqVO, Long userId); + + /** + * 移动凭证号 + * + * @param moveReqVO 移动信息 + * @param userId 用户编号 + */ + void moveVoucher(FmsVoucherMoveReqVO moveReqVO, Long userId); + + /** + * 获得凭证 + * + * @param accountSetId 账套编号 + * @param id 凭证编号 + * @param userId 用户编号 + * @return 凭证 + */ + FmsVoucherDO getVoucher(Long accountSetId, Long id, Long userId); + + /** + * 获得凭证分页 + * + * @param pageReqVO 分页查询条件 + * @param userId 用户编号 + * @return 凭证分页 + */ + PageResult getVoucherPage(FmsVoucherPageReqVO pageReqVO, Long userId); + + /** + * 获得指定期间的凭证列表 + * + * @param accountSetId 账套编号 + * @param beginTime 开始时间 + * @param endTime 结束时间,不包含 + * @return 凭证列表 + */ + List getVoucherListByPeriod(Long accountSetId, LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获得凭证分录列表 + * + * @param voucherIds 凭证编号数组 + * @return 凭证分录列表 + */ + List getVoucherEntryList(Collection voucherIds); + + /** + * 获得指定凭证的科目发生额 + * + * @param voucherIds 凭证编号数组 + * @return 科目发生额列表 + */ + List getVoucherSubjectAmountList(Collection voucherIds); + + /** + * 获得凭证分录 Map + * + * @param voucherIds 凭证编号数组 + * @return 凭证分录 Map + */ + default Map> getVoucherEntryMap(Collection voucherIds) { + return convertMultiMap(getVoucherEntryList(voucherIds), FmsVoucherEntryDO::getVoucherId); + } + + /** + * 获得使用指定科目的凭证分录数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号数组 + * @return 凭证分录数量 + */ + Long getVoucherEntryCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 获得指定科目包含数量数据的凭证分录数量 + * + * @param accountSetId 账套编号 + * @param subjectIds 科目编号数组 + * @return 凭证分录数量 + */ + Long getVoucherEntryQuantityCountBySubjectIds(Long accountSetId, Collection subjectIds); + + /** + * 迁移科目的凭证分录 + * + * @param accountSetId 账套编号 + * @param subjectId 科目编号 + * @param targetSubject 目标科目 + */ + void updateVoucherEntrySubject(Long accountSetId, Long subjectId, FmsSubjectDO targetSubject); + + /** + * 迁移科目凭证分录的辅助核算 + * + * @param accountSetId 账套编号 + * @param subjectId 科目编号 + * @param combination 辅助核算组合 + */ + void migrateVoucherEntryAuxiliaries(Long accountSetId, Long subjectId, + FmsAuxiliaryCombinationDO combination); + + /** + * 获得使用指定辅助核算项目的凭证分录数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryItemIds 辅助核算项目编号数组 + * @return 凭证分录数量 + */ + Long getVoucherEntryCountByAuxiliaryItemIds(Long accountSetId, Collection auxiliaryItemIds); + + /** + * 获得使用指定辅助核算类别的凭证分录数量 + * + * @param accountSetId 账套编号 + * @param auxiliaryTypeId 辅助核算类别编号 + * @return 凭证分录数量 + */ + Long getVoucherEntryCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId); + + /** + * 获得使用指定凭证字的凭证数量 + * + * @param accountSetId 账套编号 + * @param voucherWordId 凭证字编号 + * @return 凭证数量 + */ + Long getVoucherCountByVoucherWordId(Long accountSetId, Long voucherWordId); + + /** + * 获得下一个凭证号 + * + * @param accountSetId 账套编号 + * @param voucherWordId 凭证字编号 + * @param voucherTime 凭证日期 + * @param userId 用户编号 + * @return 下一个凭证号 + */ + Integer getNextVoucherNumber(Long accountSetId, Long voucherWordId, LocalDateTime voucherTime, Long userId); + + /** + * 获得凭证导入模板数据 + * + * @param accountSetId 账套编号 + * @param userId 用户编号 + * @return 导入模板数据 + */ + FmsVoucherImportTemplateVO getVoucherImportTemplateData(Long accountSetId, Long userId); + + /** + * 导入凭证 + * + * @param accountSetId 账套编号 + * @param rows 导入分录数组 + * @param userId 用户编号 + * @return 导入结果 + */ + FmsVoucherImportRespVO importVoucher(Long accountSetId, List rows, Long userId); + + /** + * 获得凭证汇总 + * + * @param queryReqVO 查询条件 + * @param userId 用户编号 + * @return 凭证汇总 + */ + List getVoucherStatisticsList( + FmsVoucherStatisticsReqVO queryReqVO, Long userId); + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherServiceImpl.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherServiceImpl.java new file mode 100644 index 0000000..344b0ef --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherServiceImpl.java @@ -0,0 +1,1110 @@ +package cn.iocoder.yudao.module.fms.service.voucher; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherAttachmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherEntrySaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportTemplateVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherMoveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSubjectAmountVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherTidyReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.mysql.voucher.FmsVoucherEntryMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.voucher.FmsVoucherMapper; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherStatusEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryCombinationService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryItemService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import com.mzt.logapi.context.LogRecordContext; +import com.mzt.logapi.starter.annotation.LogRecord; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolationException; +import org.springframework.context.annotation.Lazy; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.count; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sumBigDecimal; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.fms.enums.LogRecordConstants.*; +import static java.util.Collections.singletonList; + +/** + * FMS 凭证 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FmsVoucherServiceImpl implements FmsVoucherService { + + @Resource + private FmsVoucherMapper voucherMapper; + @Resource + private FmsVoucherEntryMapper voucherEntryMapper; + + @Resource + private FmsAccountSetService accountSetService; + @Resource + @Lazy // 延迟加载,避免与结账 Service 循环依赖 + private FmsClosingPeriodService closingPeriodService; + @Resource + @Lazy // 延迟加载,避免与结转凭证 Service 循环依赖 + private FmsClosingVoucherService closingVoucherService; + @Resource + private FmsVoucherWordService voucherWordService; + @Resource + private FmsSubjectService subjectService; + @Resource + private FmsAuxiliaryItemService auxiliaryItemService; + @Resource + private FmsAuxiliaryTypeService auxiliaryTypeService; + @Resource + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + + // ==================== 凭证写操作 ==================== + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_TYPE, subType = FMS_VOUCHER_CREATE_SUB_TYPE, + bizNo = "{{#voucherId}}", success = FMS_VOUCHER_CREATE_SUCCESS) + public Long createVoucher(FmsVoucherSaveReqVO createReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId); + // 1.2 串行化同一账套的凭证编号写操作 + accountSetService.lockAccountSet(createReqVO.getAccountSetId()); + // 1.3 校验会计期间 + closingPeriodService.validatePeriodOpen(createReqVO.getAccountSetId(), createReqVO.getVoucherTime()); + // 1.4 校验凭证字 + voucherWordService.validateVoucherWordExists(createReqVO.getAccountSetId(), createReqVO.getVoucherWordId()); + // 1.5 校验凭证号唯一 + LocalDateTime voucherTime = normalizeVoucherTime(createReqVO.getVoucherTime()); + validateVoucherNumberUnique(null, createReqVO.getAccountSetId(), createReqVO.getVoucherWordId(), + voucherTime, createReqVO.getVoucherNumber()); + + // 2. 校验并构造凭证分录 + List entries = validateAndBuildEntries(createReqVO, userId); + BigDecimal debitAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getDebitAmount); + BigDecimal creditAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getCreditAmount); + + // 3. 创建凭证和分录 + // 3.1 创建凭证 + FmsVoucherDO voucher = BeanUtils.toBean(createReqVO, FmsVoucherDO.class) + .setId(null).setVoucherTime(voucherTime).setAttachmentUrls(Collections.emptyList()) + .setDebitAmount(debitAmount).setCreditAmount(creditAmount) + .setTotal(debitAmount) + .setStatus(FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + try { + voucherMapper.insert(voucher); + } catch (DuplicateKeyException ex) { + // 兼容尚未移除凭证业务唯一索引的存量数据库 + throw exception(VOUCHER_NUMBER_DUPLICATE); + } + // 3.2 创建凭证分录 + entries.forEach(entry -> entry.setId(null).setVoucherId(voucher.getId())); + entries.forEach(voucherEntryMapper::insert); + + // 4. 记录操作日志上下文 + LogRecordContext.putVariable("voucherId", voucher.getId()); + return voucher.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_TYPE, subType = FMS_VOUCHER_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_VOUCHER_UPDATE_SUCCESS) + public void updateVoucher(FmsVoucherSaveReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 串行化同一账套的凭证编号写操作 + accountSetService.lockAccountSet(updateReqVO.getAccountSetId()); + // 1.3 校验凭证可编辑 + FmsVoucherDO voucher = validateVoucherExists(updateReqVO.getAccountSetId(), updateReqVO.getId()); + validateClosingVoucherEditable(updateReqVO.getAccountSetId(), singletonList(voucher.getId())); + validateVoucherEditable(voucher); + // 1.4 校验原会计期间和新会计期间 + closingPeriodService.validatePeriodOpen(updateReqVO.getAccountSetId(), voucher.getVoucherTime()); + closingPeriodService.validatePeriodOpen(updateReqVO.getAccountSetId(), updateReqVO.getVoucherTime()); + // 1.5 校验凭证字 + voucherWordService.validateVoucherWordExists(updateReqVO.getAccountSetId(), updateReqVO.getVoucherWordId()); + // 1.6 校验凭证号唯一 + LocalDateTime voucherTime = normalizeVoucherTime(updateReqVO.getVoucherTime()); + validateVoucherNumberUnique(voucher.getId(), updateReqVO.getAccountSetId(), + updateReqVO.getVoucherWordId(), voucherTime, updateReqVO.getVoucherNumber()); + + // 2. 校验并构造凭证分录 + List entries = validateAndBuildEntries(updateReqVO, userId); + BigDecimal debitAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getDebitAmount); + BigDecimal creditAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getCreditAmount); + + // 3.1 更新凭证 + FmsVoucherDO updateObj = BeanUtils.toBean(updateReqVO, FmsVoucherDO.class) + .setVoucherTime(voucherTime).setAttachmentUrls(null) + .setDebitAmount(debitAmount).setCreditAmount(creditAmount).setTotal(debitAmount); + voucherMapper.updateById(updateObj); + + // 3.2 更新凭证分录 + updateVoucherEntryList(voucher.getId(), entries); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_TYPE, subType = FMS_VOUCHER_ATTACHMENT_UPDATE_SUB_TYPE, + bizNo = "{{#updateReqVO.id}}", success = FMS_VOUCHER_ATTACHMENT_UPDATE_SUCCESS) + public void updateVoucherAttachments(FmsVoucherAttachmentUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(updateReqVO.getAccountSetId(), userId); + // 1.2 校验凭证可编辑 + FmsVoucherDO voucher = validateVoucherExists(updateReqVO.getAccountSetId(), updateReqVO.getId()); + validateClosingVoucherEditable(updateReqVO.getAccountSetId(), singletonList(voucher.getId())); + validateVoucherEditable(voucher); + // 1.3 校验会计期间 + closingPeriodService.validatePeriodOpen(updateReqVO.getAccountSetId(), voucher.getVoucherTime()); + + // 2. 更新附件 + List attachmentUrls = ObjUtil.defaultIfNull(updateReqVO.getAttachmentUrls(), Collections.emptyList()); + voucherMapper.updateById(new FmsVoucherDO().setId(voucher.getId()) + .setAttachmentUrls(attachmentUrls)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_TYPE, subType = FMS_VOUCHER_DELETE_SUB_TYPE, + bizNo = "{{#accountSetId}}", success = FMS_VOUCHER_DELETE_SUCCESS) + public void deleteVoucherList(Long accountSetId, List ids, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 串行化同一账套的凭证编号写操作 + accountSetService.lockAccountSet(accountSetId); + // 1.3 校验凭证可编辑 + List vouchers = validateVoucherList(accountSetId, ids); + validateClosingVoucherEditable(accountSetId, ids); + vouchers.forEach(this::validateVoucherEditable); + // 1.4 校验会计期间 + vouchers.forEach(voucher -> closingPeriodService.validatePeriodOpen(accountSetId, voucher.getVoucherTime())); + + // 2. 删除凭证和分录 + voucherEntryMapper.deleteByVoucherIds(ids); + voucherMapper.deleteByIds(ids); + + // 3. 记录操作日志上下文 + LogRecordContext.putVariable("voucher", CollUtil.getFirst(vouchers)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_TYPE, subType = FMS_VOUCHER_REVIEW_SUB_TYPE, + bizNo = "{{#accountSetId}}", success = FMS_VOUCHER_REVIEW_SUCCESS) + public void updateVoucherReviewStatus( + Long accountSetId, List ids, Integer status, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验目标状态 + if (ObjectUtils.notEqualsAny(status, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus(), + FmsVoucherStatusEnum.APPROVED.getStatus())) { + throw exception(VOUCHER_STATUS_INVALID); + } + // 1.3 校验凭证可审核 + List vouchers = validateVoucherList(accountSetId, ids); + validateClosingVoucherEditable(accountSetId, ids); + // 1.4 校验会计期间 + vouchers.forEach(voucher -> closingPeriodService.validatePeriodOpen( + accountSetId, voucher.getVoucherTime())); + // 1.5 校验审核状态流转 + for (FmsVoucherDO voucher : vouchers) { + boolean canApprove = ObjUtil.equal(status, FmsVoucherStatusEnum.APPROVED.getStatus()) + && ObjUtil.equal(voucher.getStatus(), FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + boolean canCancel = ObjUtil.equal(status, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + && ObjUtil.equal(voucher.getStatus(), FmsVoucherStatusEnum.APPROVED.getStatus()); + if (!canApprove && !canCancel) { + throw exception(VOUCHER_STATUS_INVALID); + } + } + + // 2. 更新审核状态和审核人 + Long reviewerUserId = ObjUtil.equal(status, FmsVoucherStatusEnum.APPROVED.getStatus()) ? userId : null; + vouchers.forEach(voucher -> voucherMapper.updateReviewStatusById( + voucher.getId(), status, reviewerUserId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_TYPE, subType = FMS_VOUCHER_TIDY_SUB_TYPE, + bizNo = "{{#tidyReqVO.accountSetId}}", success = FMS_VOUCHER_TIDY_SUCCESS) + public void tidyVoucher(FmsVoucherTidyReqVO tidyReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(tidyReqVO.getAccountSetId(), userId); + // 1.2 串行化同一账套的凭证编号写操作 + accountSetService.lockAccountSet(tidyReqVO.getAccountSetId()); + // 1.3 校验会计期间 + YearMonth month = LocalDateTimeUtils.parseYearMonth(tidyReqVO.getMonth()); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime nextMonthBeginTime = LocalDateTimeUtils.getNextMonthBeginTime(month); + closingPeriodService.validatePeriodOpen(tidyReqVO.getAccountSetId(), monthBeginTime); + // 1.4 校验凭证字 + if (tidyReqVO.getVoucherWordId() != null) { + voucherWordService.validateVoucherWordExists(tidyReqVO.getAccountSetId(), tidyReqVO.getVoucherWordId()); + } + + // 2. 按凭证字和指定方式加载待整理凭证 + List vouchers = voucherMapper.selectListForTidy( + tidyReqVO.getAccountSetId(), monthBeginTime, nextMonthBeginTime, tidyReqVO.getVoucherWordId(), + tidyReqVO.getStartNumber(), tidyReqVO.getType()); + if (CollUtil.isEmpty(vouchers)) { + return; + } + Map> voucherMap = convertMultiMap(vouchers, FmsVoucherDO::getVoucherWordId); + + // 3. 按凭证字重新生成凭证号 + List temporaryVouchers = new ArrayList<>(); + List updateVouchers = new ArrayList<>(); + for (List wordVouchers : voucherMap.values()) { + for (int index = 0; index < wordVouchers.size(); index++) { + FmsVoucherDO voucher = wordVouchers.get(index); + temporaryVouchers.add(new FmsVoucherDO().setId(voucher.getId()) + .setVoucherNumber(-index - 1)); + updateVouchers.add(new FmsVoucherDO().setId(voucher.getId()) + .setVoucherNumber(tidyReqVO.getStartNumber() + index)); + } + } + // updateBatch 仍会逐条触发凭证号唯一键校验,直接写入最终号码可能与尚未更新的原号码冲突 + // 临时负数不会占用正常凭证号,可在同一事务内安全完成号码交换和重新排序 + voucherMapper.updateBatch(temporaryVouchers); + voucherMapper.updateBatch(updateVouchers); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @LogRecord(type = FMS_VOUCHER_TYPE, subType = FMS_VOUCHER_MOVE_SUB_TYPE, + bizNo = "{{#moveReqVO.accountSetId}}", success = FMS_VOUCHER_MOVE_SUCCESS) + public void moveVoucher(FmsVoucherMoveReqVO moveReqVO, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(moveReqVO.getAccountSetId(), userId); + // 1.2 串行化同一账套的凭证编号写操作 + accountSetService.lockAccountSet(moveReqVO.getAccountSetId()); + // 1.3 校验会计期间 + YearMonth month = LocalDateTimeUtils.parseYearMonth(moveReqVO.getMonth()); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime nextMonthBeginTime = LocalDateTimeUtils.getNextMonthBeginTime(month); + closingPeriodService.validatePeriodOpen(moveReqVO.getAccountSetId(), monthBeginTime); + // 1.4 校验凭证字 + voucherWordService.validateVoucherWordExists(moveReqVO.getAccountSetId(), moveReqVO.getVoucherWordId()); + // 1.5 校验凭证编号范围 + if (moveReqVO.getTargetNumber() >= moveReqVO.getSourceNumber()) { + throw exception(VOUCHER_MOVE_RANGE_INVALID); + } + + // 2. 加载移动区间并校验原凭证 + List vouchers = voucherMapper.selectListForMove( + moveReqVO.getAccountSetId(), moveReqVO.getVoucherWordId(), + monthBeginTime, nextMonthBeginTime, + moveReqVO.getTargetNumber(), moveReqVO.getSourceNumber()); + FmsVoucherDO sourceVoucher = null; + for (FmsVoucherDO voucher : vouchers) { + if (ObjUtil.equal(voucher.getVoucherNumber(), moveReqVO.getSourceNumber())) { + if (sourceVoucher != null) { + throw exception(VOUCHER_MOVE_SOURCE_NOT_EXISTS); + } + sourceVoucher = voucher; + } + } + if (sourceVoucher == null) { + throw exception(VOUCHER_MOVE_SOURCE_NOT_EXISTS); + } + + // 3. 移动原凭证并顺延中间编号 + List temporaryVouchers = new ArrayList<>(); + for (int index = 0; index < vouchers.size(); index++) { + temporaryVouchers.add(new FmsVoucherDO().setId(vouchers.get(index).getId()).setVoucherNumber(-index - 1)); + } + Long sourceVoucherId = sourceVoucher.getId(); + List updateVouchers = convertList(vouchers, voucher -> new FmsVoucherDO().setId(voucher.getId()) + .setVoucherNumber(ObjUtil.equal(voucher.getId(), sourceVoucherId) ? moveReqVO.getTargetNumber() : voucher.getVoucherNumber() + 1)); + // updateBatch 仍会逐条触发凭证号唯一键校验,直接写入最终号码可能与尚未更新的原号码冲突 + // 临时负数不会占用正常凭证号,可在同一事务内安全完成号码移动和中间号码顺延 + voucherMapper.updateBatch(temporaryVouchers); + voucherMapper.updateBatch(updateVouchers); + } + + // ==================== 凭证查询 ==================== + + @Override + public FmsVoucherDO getVoucher(Long accountSetId, Long id, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 查询并校验凭证 + return validateVoucherExists(accountSetId, id); + } + + @Override + public PageResult getVoucherPage(FmsVoucherPageReqVO pageReqVO, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(pageReqVO.getAccountSetId(), userId); + + // 2. 查询凭证分页 + Collection voucherIds = null; + if (pageReqVO.getSubjectId() != null || StrUtil.isNotBlank(pageReqVO.getDigest()) + || pageReqVO.getMinAmount() != null || pageReqVO.getMaxAmount() != null) { + List subjectIds = pageReqVO.getSubjectId() == null ? null + : subjectService.getSubjectIdListWithChildren(pageReqVO.getAccountSetId(), pageReqVO.getSubjectId()); + List matchedEntries = voucherEntryMapper.selectVoucherIdsByCondition( + pageReqVO.getAccountSetId(), subjectIds, pageReqVO.getDigest(), + pageReqVO.getMinAmount(), pageReqVO.getMaxAmount()); + voucherIds = convertSet(matchedEntries, FmsVoucherEntryDO::getVoucherId); + if (CollUtil.isEmpty(voucherIds)) { + return PageResult.empty(); + } + } + return voucherMapper.selectPage(pageReqVO, voucherIds); + } + + @Override + public List getVoucherListByPeriod(Long accountSetId, + LocalDateTime beginTime, LocalDateTime endTime) { + return voucherMapper.selectListByAccountSetIdAndVoucherTimeBetween(accountSetId, beginTime, endTime); + } + + @Override + public List getVoucherEntryList(Collection voucherIds) { + if (CollUtil.isEmpty(voucherIds)) { + return Collections.emptyList(); + } + return voucherEntryMapper.selectListByVoucherIds(voucherIds); + } + + @Override + public List getVoucherSubjectAmountList(Collection voucherIds) { + if (CollUtil.isEmpty(voucherIds)) { + return Collections.emptyList(); + } + return voucherEntryMapper.selectSubjectAmountListByVoucherIds(voucherIds); + } + + @Override + public Long getVoucherEntryCountBySubjectIds(Long accountSetId, Collection subjectIds) { + return voucherEntryMapper.selectCountByAccountSetIdAndSubjectIds(accountSetId, subjectIds); + } + + @Override + public Long getVoucherEntryQuantityCountBySubjectIds(Long accountSetId, Collection subjectIds) { + return voucherEntryMapper.selectQuantityCountByAccountSetIdAndSubjectIds(accountSetId, subjectIds); + } + + @Override + public void updateVoucherEntrySubject(Long accountSetId, Long subjectId, FmsSubjectDO targetSubject) { + voucherEntryMapper.updateSubject(accountSetId, subjectId, + new FmsVoucherEntryDO().setSubjectId(targetSubject.getId()) + .setSubjectCode(targetSubject.getCode()).setSubjectName(targetSubject.getName())); + } + + @Override + public void migrateVoucherEntryAuxiliaries(Long accountSetId, Long subjectId, + FmsAuxiliaryCombinationDO combination) { + // 1. 查询科目的凭证分录 + List entries = voucherEntryMapper.selectListByAccountSetIdAndSubjectId( + accountSetId, subjectId); + if (CollUtil.isEmpty(entries)) { + return; + } + + // 2. 将历史分录迁移到指定辅助核算组合 + List auxiliaries = convertList(combination.getItems(), item -> + FmsVoucherEntryDO.AuxiliaryItem.builder().type(item.getType()).typeId(item.getTypeId()) + .itemId(item.getItemId()).name(item.getName()).build()); + entries.forEach(entry -> entry.setAssistCombinationId(combination.getId()).setAuxiliaries(auxiliaries)); + voucherEntryMapper.updateBatch(entries); + } + + @Override + public Long getVoucherEntryCountByAuxiliaryItemIds(Long accountSetId, Collection auxiliaryItemIds) { + if (CollUtil.isEmpty(auxiliaryItemIds)) { + return 0L; + } + Set auxiliaryItemIdSet = new LinkedHashSet<>(auxiliaryItemIds); + return getVoucherEntryCountByAuxiliary(accountSetId, + item -> auxiliaryItemIdSet.contains(item.getItemId())); + } + + @Override + public Long getVoucherEntryCountByAuxiliaryTypeId(Long accountSetId, Long auxiliaryTypeId) { + return getVoucherEntryCountByAuxiliary(accountSetId, + item -> ObjUtil.equal(item.getTypeId(), auxiliaryTypeId)); + } + + @Override + public Long getVoucherCountByVoucherWordId(Long accountSetId, Long voucherWordId) { + return voucherMapper.selectCountByAccountSetIdAndVoucherWordId(accountSetId, voucherWordId); + } + + @Override + public Integer getNextVoucherNumber(Long accountSetId, Long voucherWordId, + LocalDateTime voucherTime, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + // 1.2 校验凭证字 + voucherWordService.validateVoucherWordExists(accountSetId, voucherWordId); + + // 2. 计算下一个凭证号 + return calculateNextVoucherNumber(accountSetId, voucherWordId, voucherTime); + } + + // ==================== 凭证导入 ==================== + + @Override + public FmsVoucherImportTemplateVO getVoucherImportTemplateData(Long accountSetId, Long userId) { + // 1. 校验账套读权限 + accountSetService.validateAccountSetReadPermission(accountSetId, userId); + + // 2. 加载导入模板依赖 + List voucherWords = voucherWordService.getVoucherWordList(accountSetId); + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + List auxiliaryTypes = auxiliaryTypeService.getAuxiliaryTypeList(accountSetId, userId); + List auxiliaryItems = auxiliaryItemService.getAuxiliaryItemListByAccountSetId(accountSetId, userId); + return new FmsVoucherImportTemplateVO(voucherWords, subjects, auxiliaryTypes, auxiliaryItems); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FmsVoucherImportRespVO importVoucher(Long accountSetId, List rows, Long userId) { + // 1.1 校验账套写权限 + accountSetService.validateAccountSetWritePermission(accountSetId, userId); + // 1.2 校验导入数据非空 + if (CollUtil.isEmpty(rows)) { + throw exception(VOUCHER_IMPORT_FILE_EMPTY); + } + // 1.3 加载导入依赖 + Map voucherWordMap = voucherWordService.getVoucherWordMapByName(accountSetId); + List subjects = subjectService.getSubjectList(accountSetId, null, userId); + Map auxiliaryTypeMap = auxiliaryTypeService.getAuxiliaryTypeMap(accountSetId, userId); + Map auxiliaryItemMap = auxiliaryItemService.getAuxiliaryItemMapByTypeIdAndCode(accountSetId, userId); + Map subjectMap = convertMap(subjects, FmsSubjectDO::getCode); + Set parentSubjectIds = convertSet(subjects, FmsSubjectDO::getParentId); + + // 2. 按日期、凭证字和凭证号分组并校验每条分录 + Map> groupMap = new LinkedHashMap<>(); + for (FmsVoucherImportExcelVO row : rows) { + validateImportRow(row, voucherWordMap, subjectMap, parentSubjectIds, auxiliaryTypeMap, auxiliaryItemMap); + groupMap.computeIfAbsent(buildImportGroupKey(row), key -> new ArrayList<>()).add(row); + } + + // 3. 校验凭证组摘要、期间、借贷平衡和业务键 + Set importBusinessKeys = new LinkedHashSet<>(); + for (List groupRows : groupMap.values()) { + validateImportGroup(accountSetId, groupRows, voucherWordMap); + String businessKey = buildImportBusinessKey(CollUtil.getFirst(groupRows)); + if (businessKey != null && !importBusinessKeys.add(businessKey)) { + addGroupError(groupRows, "同一会计期间、凭证字和凭证号重复"); + } + } + + // 4. 落库有效凭证组 + int successVoucherCount = 0; + List errorRows = new ArrayList<>(); + for (List groupRows : groupMap.values()) { + if (groupRows.stream().anyMatch(row -> CollUtil.isNotEmpty(row.getErrors()))) { + errorRows.addAll(groupRows); + continue; + } + insertImportVoucher(accountSetId, groupRows, voucherWordMap, subjectMap, auxiliaryTypeMap, auxiliaryItemMap); + successVoucherCount++; + } + return new FmsVoucherImportRespVO().setTotalRowCount(rows.size()) + .setSuccessRowCount(rows.size() - errorRows.size()).setFailureRowCount(errorRows.size()) + .setTotalVoucherCount(groupMap.size()).setSuccessVoucherCount(successVoucherCount) + .setFailureVoucherCount(groupMap.size() - successVoucherCount).setErrorRows(errorRows); + } + + // ==================== 凭证汇总 ==================== + + @Override + public List getVoucherStatisticsList( + FmsVoucherStatisticsReqVO queryReqVO, Long userId) { + // 1.1 校验账套读权限 + accountSetService.validateAccountSetReadPermission(queryReqVO.getAccountSetId(), userId); + // 1.2 校验会计期间 + YearMonth startMonth = LocalDateTimeUtils.parseYearMonth(queryReqVO.getStartMonth()); + YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(queryReqVO.getEndMonth()); + if (startMonth.isAfter(endMonth)) { + throw exception(LEDGER_PERIOD_INVALID); + } + // 1.3 校验科目层级和凭证号范围 + int minLevel = ObjUtil.defaultIfNull(queryReqVO.getMinLevel(), 1); + int maxLevel = ObjUtil.defaultIfNull(queryReqVO.getMaxLevel(), 1); + if (minLevel > maxLevel || queryReqVO.getMinVoucherNumber() != null + && queryReqVO.getMaxVoucherNumber() != null + && queryReqVO.getMinVoucherNumber() > queryReqVO.getMaxVoucherNumber()) { + throw exception(VOUCHER_STATISTICS_RANGE_INVALID); + } + // 1.4 校验凭证字 + if (queryReqVO.getVoucherWordId() != null) { + voucherWordService.validateVoucherWordExists( + queryReqVO.getAccountSetId(), queryReqVO.getVoucherWordId()); + } + + // 2. 查询科目直接发生额 + LocalDateTime beginTime = LocalDateTimeUtils.getMonthBeginTime(startMonth); + LocalDateTime endTime = LocalDateTimeUtils.getNextMonthBeginTime(endMonth); + List amounts = voucherEntryMapper.selectSubjectAmountList( + queryReqVO.getAccountSetId(), beginTime, endTime, queryReqVO.getVoucherWordId(), + queryReqVO.getMinVoucherNumber(), queryReqVO.getMaxVoucherNumber()); + Map amountMap = convertMap( + amounts, FmsVoucherSubjectAmountVO::getSubjectId); + if (CollUtil.isEmpty(amountMap)) { + return new ArrayList<>(); + } + + // 3. 按科目级次汇总自身和全部下级发生额 + List subjects = subjectService.getSubjectList(queryReqVO.getAccountSetId(), null, userId); + Map> childMap = convertMultiMap( + filterList(subjects, subject -> subject.getParentId() != null), + FmsSubjectDO::getParentId, FmsSubjectDO::getId); + List result = new ArrayList<>(); + for (FmsSubjectDO subject : subjects) { + if (subject.getLevel() < minLevel || subject.getLevel() > maxLevel) { + continue; + } + Set subjectIds = new LinkedHashSet<>(); + Deque pendingSubjectIds = new ArrayDeque<>(); + pendingSubjectIds.add(subject.getId()); + while (CollUtil.isNotEmpty(pendingSubjectIds)) { + Long subjectId = pendingSubjectIds.removeFirst(); + if (subjectIds.add(subjectId)) { + pendingSubjectIds.addAll(CollUtil.emptyIfNull(childMap.get(subjectId))); + } + } + BigDecimal debitAmount = BigDecimal.ZERO; + BigDecimal creditAmount = BigDecimal.ZERO; + boolean hasAmount = false; + for (Long subjectId : subjectIds) { + FmsVoucherSubjectAmountVO amount = amountMap.get(subjectId); + if (amount != null) { + debitAmount = debitAmount.add(amount.getDebitAmount()); + creditAmount = creditAmount.add(amount.getCreditAmount()); + hasAmount = true; + } + } + if (hasAmount) { + result.add(new FmsVoucherStatisticsRespVO().setSubjectId(subject.getId()) + .setSubjectCode(subject.getCode()).setSubjectName(subject.getName()) + .setLevel(subject.getLevel()).setDebitAmount(debitAmount).setCreditAmount(creditAmount)); + } + } + return result; + } + + /** + * 校验凭证导入行 + * + * @param row 导入行 + * @param voucherWordMap 凭证字 Map + * @param subjectMap 科目 Map + * @param parentSubjectIds 非末级科目编号数组 + * @param auxiliaryTypeMap 辅助核算类别 Map + * @param auxiliaryItemMap 辅助核算项目 Map + */ + private void validateImportRow(FmsVoucherImportExcelVO row, + Map voucherWordMap, Map subjectMap, + Set parentSubjectIds, Map auxiliaryTypeMap, + Map auxiliaryItemMap) { + // 1. 校验 Excel 字段格式 + try { + ValidationUtils.validate(row); + } catch (ConstraintViolationException exception) { + row.addError(exception.getMessage()); + } + + // 2. 校验凭证字和科目 + if (StrUtil.isNotBlank(row.getVoucherWordName()) + && !voucherWordMap.containsKey(row.getVoucherWordName())) { + row.addError("凭证字不存在"); + } + FmsSubjectDO subject = subjectMap.get(row.getSubjectCode()); + if (subject == null) { + row.addError("科目编码不存在"); + } else if (ObjUtil.notEqual(subject.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + row.addError("科目已停用"); + } else if (parentSubjectIds.contains(subject.getId())) { + row.addError("只能导入末级科目"); + } + // 3. 校验借贷金额、数量核算和辅助核算 + BigDecimal debitAmount = normalizeAmount(row.getDebitAmount()); + BigDecimal creditAmount = normalizeAmount(row.getCreditAmount()); + if ((debitAmount.signum() != 0 && creditAmount.signum() != 0) + || (debitAmount.signum() == 0 && creditAmount.signum() == 0)) { + row.addError("借方金额和贷方金额只能填写一项且金额不能为 0"); + } + if (subject != null) { + validateImportQuantity(row, subject, debitAmount.abs().max(creditAmount.abs())); + validateImportAuxiliaries(row, subject, auxiliaryTypeMap, auxiliaryItemMap); + } + } + + private void validateImportQuantity( + FmsVoucherImportExcelVO row, FmsSubjectDO subject, BigDecimal amount) { + BigDecimal quantity = ObjUtil.defaultIfNull(row.getQuantity(), BigDecimal.ZERO); + BigDecimal unitPrice = ObjUtil.defaultIfNull(row.getUnitPrice(), BigDecimal.ZERO); + if (Boolean.FALSE.equals(subject.getQuantityAccounting())) { + if (quantity.signum() > 0 || unitPrice.signum() > 0) { + row.addError("该科目未启用数量核算,不能填写数量和单价"); + } + return; + } + if (quantity.signum() == 0 && unitPrice.signum() == 0) { + return; + } + if (quantity.signum() <= 0 || unitPrice.signum() <= 0 + || quantity.multiply(unitPrice).setScale(2, RoundingMode.FLOOR).compareTo(amount) != 0) { + row.addError("数量和单价需同时大于 0,且乘积应等于分录金额"); + } + } + + /** + * 校验凭证导入行的辅助核算项目 + * + * @param row 导入行 + * @param subject 科目 + * @param auxiliaryTypeMap 辅助核算类别 Map + * @param auxiliaryItemMap 辅助核算项目 Map + */ + private void validateImportAuxiliaries(FmsVoucherImportExcelVO row, FmsSubjectDO subject, + Map auxiliaryTypeMap, + Map auxiliaryItemMap) { + List auxiliaryTypeIds = ObjUtil.defaultIfNull(subject.getAuxiliaryTypeIds(), Collections.emptyList()); + // 按科目要求的辅助核算类别逐项校验项目编码和启用状态 + for (Long auxiliaryTypeId : auxiliaryTypeIds) { + String code = row.getAuxiliaryCodes().get(auxiliaryTypeId); + FmsAuxiliaryItemDO item = auxiliaryItemMap.get(buildAuxiliaryItemKey(auxiliaryTypeId, code)); + String typeName = auxiliaryTypeMap.get(auxiliaryTypeId).getName(); + if (StrUtil.isBlank(code) || item == null + || ObjUtil.notEqual(item.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + row.addError(typeName + "辅助核算项目不存在或未填写"); + } + } + } + + /** + * 校验同一凭证的导入分录 + * + * @param accountSetId 账套编号 + * @param groupRows 同一凭证的导入行数组 + * @param voucherWordMap 凭证字 Map + */ + private void validateImportGroup(Long accountSetId, List groupRows, + Map voucherWordMap) { + FmsVoucherImportExcelVO firstRow = CollUtil.getFirst(groupRows); + // 1. 校验分录数量、摘要和借贷平衡 + if (groupRows.size() < 2) { + addGroupError(groupRows, "凭证至少需要两条分录"); + } + if (groupRows.stream().noneMatch(row -> StrUtil.isNotBlank(row.getDigest()))) { + addGroupError(groupRows, "同一凭证至少填写一条摘要"); + } + BigDecimal debitAmount = sumBigDecimal(groupRows, row -> normalizeAmount(row.getDebitAmount())); + BigDecimal creditAmount = sumBigDecimal(groupRows, row -> normalizeAmount(row.getCreditAmount())); + if (debitAmount.compareTo(creditAmount) != 0) { + addGroupError(groupRows, "凭证借贷金额不平衡"); + } + // 2. 校验会计期间和凭证号唯一 + if (firstRow.getVoucherTime() == null) { + return; + } + if (closingPeriodService.isPeriodClosed(accountSetId, firstRow.getVoucherTime())) { + addGroupError(groupRows, "凭证所在会计期间已结账"); + } + FmsVoucherWordDO voucherWord = voucherWordMap.get(firstRow.getVoucherWordName()); + if (voucherWord != null && firstRow.getVoucherNumber() != null + && firstRow.getVoucherNumber() > 0) { + YearMonth month = YearMonth.from(firstRow.getVoucherTime()); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime nextMonthBeginTime = LocalDateTimeUtils.getNextMonthBeginTime(month); + FmsVoucherDO voucher = voucherMapper.selectByAccountSetIdAndVoucherWordIdAndVoucherNumberAndVoucherTimeBetween( + accountSetId, voucherWord.getId(), firstRow.getVoucherNumber(), + monthBeginTime, nextMonthBeginTime); + if (voucher != null) { + addGroupError(groupRows, "同一会计期间、凭证字和凭证号已存在"); + } + } + } + + /** + * 创建导入凭证及其分录 + * + * @param accountSetId 账套编号 + * @param groupRows 同一凭证的导入行数组 + * @param voucherWordMap 凭证字 Map + * @param subjectMap 科目 Map + * @param auxiliaryTypeMap 辅助核算类别 Map + * @param auxiliaryItemMap 辅助核算项目 Map + */ + private void insertImportVoucher(Long accountSetId, List groupRows, + Map voucherWordMap, Map subjectMap, + Map auxiliaryTypeMap, + Map auxiliaryItemMap) { + FmsVoucherImportExcelVO firstRow = CollUtil.getFirst(groupRows); + // 1. 构造凭证分录和辅助核算快照 + List entries = new ArrayList<>(); + for (int index = 0; index < groupRows.size(); index++) { + FmsVoucherImportExcelVO row = groupRows.get(index); + FmsSubjectDO subject = subjectMap.get(row.getSubjectCode()); + List auxiliaries = new ArrayList<>(); + List auxiliaryTypeIds = ObjUtil.defaultIfNull(subject.getAuxiliaryTypeIds(), Collections.emptyList()); + for (Long auxiliaryTypeId : auxiliaryTypeIds) { + FmsAuxiliaryItemDO item = auxiliaryItemMap.get( + buildAuxiliaryItemKey(auxiliaryTypeId, row.getAuxiliaryCodes().get(auxiliaryTypeId))); + auxiliaries.add(FmsVoucherEntryDO.AuxiliaryItem.builder() + .type(auxiliaryTypeMap.get(auxiliaryTypeId).getType()) + .typeId(auxiliaryTypeId).itemId(item.getId()) + .name(item.getName()).build()); + } + BigDecimal rowDebitAmount = normalizeAmount(row.getDebitAmount()); + BigDecimal rowCreditAmount = normalizeAmount(row.getCreditAmount()); + entries.add(new FmsVoucherEntryDO().setAccountSetId(accountSetId) + .setDigest(row.getDigest()).setSubjectId(subject.getId()) + .setSubjectCode(subject.getCode()).setSubjectName(subject.getName()) + .setQuantity(ObjUtil.defaultIfNull(row.getQuantity(), BigDecimal.ZERO)) + .setUnitPrice(ObjUtil.defaultIfNull(row.getUnitPrice(), BigDecimal.ZERO)) + .setDebitAmount(rowDebitAmount).setCreditAmount(rowCreditAmount) + .setSort(index + 1).setAuxiliaries(auxiliaries)); + } + auxiliaryCombinationService.setAuxiliaryCombinationIds(accountSetId, entries); + + // 2.1 创建凭证 + BigDecimal debitAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getDebitAmount); + BigDecimal creditAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getCreditAmount); + FmsVoucherDO voucher = new FmsVoucherDO().setAccountSetId(accountSetId) + .setVoucherWordId(voucherWordMap.get(firstRow.getVoucherWordName()).getId()) + .setVoucherNumber(firstRow.getVoucherNumber()).setVoucherTime(normalizeVoucherTime(firstRow.getVoucherTime())) + .setAttachmentUrls(new ArrayList<>()) + .setAttachmentCount(ObjUtil.defaultIfNull(firstRow.getAttachmentCount(), 0)) + .setDebitAmount(debitAmount).setCreditAmount(creditAmount) + .setTotal(debitAmount) + .setStatus(FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(voucher); + + // 2.2 创建凭证分录 + entries.forEach(entry -> entry.setVoucherId(voucher.getId())); + entries.forEach(voucherEntryMapper::insert); + } + + private String buildImportGroupKey(FmsVoucherImportExcelVO row) { + if (row.getVoucherTime() == null || StrUtil.isBlank(row.getVoucherWordName()) + || row.getVoucherNumber() == null) { + return "第" + row.getRowNumber() + "行"; + } + return row.getVoucherTime() + "|" + row.getVoucherWordName() + "|" + row.getVoucherNumber(); + } + + private String buildImportBusinessKey(FmsVoucherImportExcelVO row) { + if (row.getVoucherTime() == null || StrUtil.isBlank(row.getVoucherWordName()) + || row.getVoucherNumber() == null) { + return null; + } + return YearMonth.from(row.getVoucherTime()) + "|" + row.getVoucherWordName() + "|" + row.getVoucherNumber(); + } + + private String buildAuxiliaryItemKey(Long auxiliaryTypeId, String code) { + return auxiliaryTypeId + "|" + code; + } + + private void addGroupError(List rows, String error) { + rows.forEach(row -> row.addError(error)); + } + + /** + * 计算指定会计期间和凭证字的下一个凭证号 + * + * @param accountSetId 账套编号 + * @param voucherWordId 凭证字编号 + * @param voucherTime 凭证日期 + * @return 下一个凭证号 + */ + private Integer calculateNextVoucherNumber( + Long accountSetId, Long voucherWordId, LocalDateTime voucherTime) { + YearMonth month = YearMonth.from(voucherTime); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime nextMonthBeginTime = LocalDateTimeUtils.getNextMonthBeginTime(month); + FmsVoucherDO lastVoucher = voucherMapper.selectLastByPeriod( + accountSetId, voucherWordId, monthBeginTime, nextMonthBeginTime); + return lastVoucher == null ? 1 : lastVoucher.getVoucherNumber() + 1; + } + + /** + * 校验并构造凭证分录 + * + * @param reqVO 凭证保存信息 + * @param userId 用户编号 + * @return 凭证分录数组 + */ + private List validateAndBuildEntries(FmsVoucherSaveReqVO reqVO, Long userId) { + if (CollUtil.size(reqVO.getEntries()) < 2) { + throw exception(VOUCHER_ENTRY_REQUIRED); + } + + // 1. 批量加载科目和辅助核算项目 + List allSubjects = subjectService.getSubjectList(reqVO.getAccountSetId(), null, userId); + Map subjectMap = convertMap(allSubjects, FmsSubjectDO::getId); + Set parentIds = convertSet(allSubjects, FmsSubjectDO::getParentId); + Set auxiliaryTypeIds = convertSetByFlatMap( + allSubjects, FmsSubjectDO::getAuxiliaryTypeIds, Collection::stream); + List auxiliaryTypes = auxiliaryTypeService.validateAuxiliaryTypeList( + reqVO.getAccountSetId(), auxiliaryTypeIds); + Map auxiliaryTypeMap = convertMap(auxiliaryTypes, FmsAuxiliaryTypeDO::getId); + Set auxiliaryItems = convertSetByFlatMap( + reqVO.getEntries(), FmsVoucherEntrySaveReqVO::getAuxiliaries, Collection::stream); + Set auxiliaryItemIds = convertSet(auxiliaryItems, FmsVoucherEntrySaveReqVO.AuxiliaryItem::getItemId); + List auxiliaryItemList = auxiliaryItemService.validateAuxiliaryItemList( + reqVO.getAccountSetId(), auxiliaryItemIds); + Map auxiliaryItemMap = convertMap(auxiliaryItemList, FmsAuxiliaryItemDO::getId); + + // 2. 校验并构造每条分录 + List entries = new ArrayList<>(); + for (int index = 0; index < reqVO.getEntries().size(); index++) { + FmsVoucherEntrySaveReqVO entryReqVO = reqVO.getEntries().get(index); + FmsSubjectDO subject = subjectMap.get(entryReqVO.getSubjectId()); + validateVoucherSubject(subject, parentIds); + BigDecimal debit = normalizeAmount(entryReqVO.getDebitAmount()); + BigDecimal credit = normalizeAmount(entryReqVO.getCreditAmount()); + if ((debit.signum() != 0 && credit.signum() != 0) + || (debit.signum() == 0 && credit.signum() == 0)) { + throw exception(VOUCHER_ENTRY_AMOUNT_INVALID); + } + validateQuantity(entryReqVO, subject, debit.abs().max(credit.abs())); + List auxiliaries = buildAuxiliaries( + entryReqVO, subject, auxiliaryTypeMap, auxiliaryItemMap); + // 数量和单价仅在科目启用数量核算且实际填写时必填,未启用时保持零值 + entries.add(new FmsVoucherEntryDO().setAccountSetId(reqVO.getAccountSetId()) + .setId(entryReqVO.getId()) + .setDigest(entryReqVO.getDigest()).setSubjectId(subject.getId()) + .setSubjectCode(subject.getCode()).setSubjectName(subject.getName()) + .setQuantity(ObjUtil.defaultIfNull(entryReqVO.getQuantity(), BigDecimal.ZERO)) + .setUnitPrice(ObjUtil.defaultIfNull(entryReqVO.getUnitPrice(), BigDecimal.ZERO)) + .setDebitAmount(debit).setCreditAmount(credit).setSort(index + 1) + .setAuxiliaries(auxiliaries)); + } + + // 3. 校验凭证借贷平衡并补充辅助核算组合编号 + BigDecimal debitAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getDebitAmount); + BigDecimal creditAmount = sumBigDecimal(entries, FmsVoucherEntryDO::getCreditAmount); + if (debitAmount.compareTo(creditAmount) != 0) { + throw exception(VOUCHER_AMOUNT_UNBALANCED); + } + auxiliaryCombinationService.setAuxiliaryCombinationIds(reqVO.getAccountSetId(), entries); + return entries; + } + + /** + * 差量更新凭证分录 + * + * @param voucherId 凭证编号 + * @param newEntries 新凭证分录数组 + */ + private void updateVoucherEntryList(Long voucherId, List newEntries) { + // 1. 对比新老分录,获得新增、修改、删除列表 + List oldEntries = voucherEntryMapper.selectListByVoucherId(voucherId); + List> diffEntries = diffList(oldEntries, newEntries, + (oldEntry, newEntry) -> ObjUtil.equal(oldEntry.getId(), newEntry.getId())); + + // 2. 新增、修改、删除分录 + if (CollUtil.isNotEmpty(diffEntries.get(0))) { + diffEntries.get(0).forEach(entry -> entry.setId(null).setVoucherId(voucherId)); + diffEntries.get(0).forEach(voucherEntryMapper::insert); + } + if (CollUtil.isNotEmpty(diffEntries.get(1))) { + diffEntries.get(1).forEach(entry -> entry.setVoucherId(voucherId)); + voucherEntryMapper.updateBatch(diffEntries.get(1)); + } + if (CollUtil.isNotEmpty(diffEntries.get(2))) { + voucherEntryMapper.deleteByIds(convertList(diffEntries.get(2), FmsVoucherEntryDO::getId)); + } + } + + private void validateVoucherSubject(FmsSubjectDO subject, Set parentIds) { + if (subject == null) { + throw exception(SUBJECT_NOT_EXISTS); + } + if (ObjUtil.notEqual(subject.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(VOUCHER_SUBJECT_DISABLED); + } + if (parentIds.contains(subject.getId())) { + throw exception(VOUCHER_SUBJECT_HAS_CHILDREN); + } + } + + /** + * 校验分录数量和单价 + * + * @param entryReqVO 分录保存信息 + * @param subject 科目 + * @param amount 分录金额 + */ + private void validateQuantity(FmsVoucherEntrySaveReqVO entryReqVO, + FmsSubjectDO subject, BigDecimal amount) { + BigDecimal quantity = entryReqVO.getQuantity(); + BigDecimal unitPrice = entryReqVO.getUnitPrice(); + // 未启用数量核算的科目不能填写数量和单价 + if (Boolean.FALSE.equals(subject.getQuantityAccounting())) { + if (quantity != null && quantity.signum() > 0 || unitPrice != null && unitPrice.signum() > 0) { + throw exception(VOUCHER_QUANTITY_INVALID); + } + return; + } + // 数量和单价均未填写时,不进行数量金额校验 + if ((quantity == null || quantity.signum() == 0) + && (unitPrice == null || unitPrice.signum() == 0)) { + return; + } + // 数量和单价需同时大于零,且乘积必须等于分录金额 + if (quantity == null || quantity.signum() <= 0 || unitPrice == null || unitPrice.signum() <= 0 + || quantity.multiply(unitPrice).setScale(2, RoundingMode.FLOOR).compareTo(amount) != 0) { + throw exception(VOUCHER_QUANTITY_INVALID); + } + } + + /** + * 校验并构造分录辅助核算项目快照 + * + * @param entryReqVO 分录保存信息 + * @param subject 科目 + * @param auxiliaryTypeMap 辅助核算类别 Map + * @param auxiliaryItemMap 辅助核算项目 Map + * @return 辅助核算项目快照数组 + */ + private List buildAuxiliaries(FmsVoucherEntrySaveReqVO entryReqVO, FmsSubjectDO subject, + Map auxiliaryTypeMap, + Map auxiliaryItemMap) { + List auxiliaryTypeIds = ObjUtil.defaultIfNull(subject.getAuxiliaryTypeIds(), Collections.emptyList()); + List requested = ObjUtil.defaultIfNull(entryReqVO.getAuxiliaries(), Collections.emptyList()); + Map requestedMap = convertMap( + requested, FmsVoucherEntrySaveReqVO.AuxiliaryItem::getTypeId); + if (auxiliaryTypeIds.size() != requested.size() || requestedMap.size() != requested.size()) { + throw exception(VOUCHER_AUXILIARY_REQUIRED); + } + + // 按科目配置顺序校验项目归属和启用状态,并保存名称快照 + List auxiliaries = new ArrayList<>(); + for (Long auxiliaryTypeId : auxiliaryTypeIds) { + FmsVoucherEntrySaveReqVO.AuxiliaryItem requestItem = requestedMap.get(auxiliaryTypeId); + FmsAuxiliaryItemDO item = requestItem == null ? null + : auxiliaryItemMap.get(requestItem.getItemId()); + if (item == null || ObjUtil.notEqual(item.getAuxiliaryTypeId(), auxiliaryTypeId) + || ObjUtil.notEqual(item.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(VOUCHER_AUXILIARY_REQUIRED); + } + auxiliaries.add(FmsVoucherEntryDO.AuxiliaryItem.builder() + .type(auxiliaryTypeMap.get(auxiliaryTypeId).getType()) + .typeId(auxiliaryTypeId).itemId(item.getId()) + .name(item.getName()).build()); + } + return auxiliaries; + } + + private List validateVoucherList(Long accountSetId, List ids) { + List vouchers = voucherMapper.selectListByIdsAndAccountSetId(ids, accountSetId); + if (vouchers.size() != ids.size()) { + throw exception(VOUCHER_NOT_EXISTS); + } + return vouchers; + } + + private FmsVoucherDO validateVoucherExists(Long accountSetId, Long id) { + FmsVoucherDO voucher = voucherMapper.selectById(id); + if (voucher == null || ObjUtil.notEqual(voucher.getAccountSetId(), accountSetId)) { + throw exception(VOUCHER_NOT_EXISTS); + } + return voucher; + } + + private Long getVoucherEntryCountByAuxiliary(Long accountSetId, + Predicate predicate) { + List entries = voucherEntryMapper.selectListByAccountSetId(accountSetId); + return count(entries, entry -> CollUtil.isNotEmpty(entry.getAuxiliaries()) + && entry.getAuxiliaries().stream().anyMatch(predicate)); + } + + private void validateVoucherEditable(FmsVoucherDO voucher) { + if (ObjUtil.equal(voucher.getStatus(), FmsVoucherStatusEnum.APPROVED.getStatus())) { + throw exception(VOUCHER_APPROVED_NOT_EDITABLE); + } + } + + private void validateClosingVoucherEditable(Long accountSetId, Collection voucherIds) { + if (CollUtil.isNotEmpty(closingVoucherService.getClosingVoucherIdSet(accountSetId, voucherIds))) { + throw exception(VOUCHER_CLOSING_GENERATED_NOT_EDITABLE); + } + } + + private void validateVoucherNumberUnique(Long id, Long accountSetId, Long voucherWordId, + LocalDateTime voucherTime, Integer voucherNumber) { + YearMonth month = YearMonth.from(voucherTime); + LocalDateTime monthBeginTime = LocalDateTimeUtils.getMonthBeginTime(month); + LocalDateTime nextMonthBeginTime = LocalDateTimeUtils.getNextMonthBeginTime(month); + FmsVoucherDO voucher = voucherMapper.selectByAccountSetIdAndVoucherWordIdAndVoucherNumberAndVoucherTimeBetween( + accountSetId, voucherWordId, voucherNumber, + monthBeginTime, nextMonthBeginTime); + if (voucher != null && ObjUtil.notEqual(voucher.getId(), id)) { + throw exception(VOUCHER_NUMBER_DUPLICATE); + } + } + + private LocalDateTime normalizeVoucherTime(LocalDateTime voucherTime) { + return LocalDateTimeUtils.beginOfDay(voucherTime); + } + + private BigDecimal normalizeAmount(BigDecimal amount) { + return MoneyUtils.priceScale(amount); + } + +} diff --git a/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/util/FmsPeriodUtils.java b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/util/FmsPeriodUtils.java new file mode 100644 index 0000000..68b1b30 --- /dev/null +++ b/yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/util/FmsPeriodUtils.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.fms.util; + +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import lombok.experimental.UtilityClass; + +import java.time.YearMonth; + +/** + * FMS 会计期间工具类 + * + * @author 芋道源码 + */ +@UtilityClass +public final class FmsPeriodUtils { + + /** + * 获得报表和结账内部使用的本年起始期间。 + * + * 年初取数仍然按自然年计算;账套启用当年从启用期间开始,避免内部查询落到账套启用前的期间。 + * + * @param accountSet 账套 + * @param endMonth 结束期间 + * @return 有效的本年起始期间 + */ + public static YearMonth getYearStartMonth(FmsAccountSetDO accountSet, YearMonth endMonth) { + YearMonth yearStartMonth = YearMonth.of(endMonth.getYear(), 1); + if (accountSet == null || accountSet.getStartTime() == null) { + return yearStartMonth; + } + YearMonth accountStartMonth = YearMonth.from(accountSet.getStartTime()); + return accountStartMonth.isAfter(yearStartMonth) ? accountStartMonth : yearStartMonth; + } + +} diff --git a/yudao-module-fms/src/main/resources/fms/closing/closing-scheme-presets.json b/yudao-module-fms/src/main/resources/fms/closing/closing-scheme-presets.json new file mode 100644 index 0000000..742dbc8 --- /dev/null +++ b/yudao-module-fms/src/main/resources/fms/closing/closing-scheme-presets.json @@ -0,0 +1,114 @@ +[ + { + "code": "profit-loss", + "name": "结转损益", + "closingType": "PROFIT_LOSS", + "digest": "结转本期损益", + "periodEnd": true, + "closingDay": 31, + "voucherType": "COMBINED_GAIN_AND_LOSS", + "reverseBalance": true, + "priorYearAdjustmentSubject": "PRIOR_YEAR_ADJUSTMENT", + "adjustmentClosingSubject": "PRIOR_YEAR_ADJUSTMENT_CLOSING", + "otherClosingSubject": "CURRENT_YEAR_PROFIT", + "entries": [] + }, + { + "code": "unpaid-vat", + "name": "转出未交增值税", + "closingType": "UNPAID_VAT", + "digest": "转出未交增值税", + "periodEnd": true, + "closingDay": 31, + "calculationSubjects": [ + "VAT_PAYABLE" + ], + "entries": [ + { + "subject": "UNPAID_VAT", + "digest": "转出未交增值税", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subject": "OUTPUT_VAT", + "digest": "转出未交增值税", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "local-tax", + "name": "计提地税", + "closingType": "LOCAL_TAX", + "digest": "计提地税", + "periodEnd": true, + "closingDay": 31, + "calculationSubjects": [ + "VAT_PAYABLE", + "CONSUMPTION_TAX" + ], + "entries": [ + { + "subject": "EDUCATION_SURCHARGE", + "digest": "计提教育费附加", + "direction": "DEBIT", + "amountRatio": 3 + }, + { + "subject": "EDUCATION_SURCHARGE_PAYABLE", + "digest": "计提教育费附加", + "direction": "CREDIT", + "amountRatio": 3 + }, + { + "subject": "CITY_MAINTENANCE_TAX", + "digest": "计提城建费", + "direction": "DEBIT", + "amountRatio": 7 + }, + { + "subject": "CITY_MAINTENANCE_TAX_PAYABLE", + "digest": "计提城建费", + "direction": "CREDIT", + "amountRatio": 7 + }, + { + "subject": "LOCAL_EDUCATION_SURCHARGE", + "digest": "计提地方教育费附加", + "direction": "DEBIT", + "amountRatio": 2 + }, + { + "subject": "LOCAL_EDUCATION_SURCHARGE_PAYABLE", + "digest": "计提地方教育费附加", + "direction": "CREDIT", + "amountRatio": 2 + } + ] + }, + { + "code": "income-tax", + "name": "计提所得税", + "closingType": "INCOME_TAX", + "digest": "计提所得税", + "periodEnd": true, + "closingDay": 31, + "calculationSubjects": [], + "entries": [ + { + "subject": "INCOME_TAX_EXPENSE", + "digest": "计提所得税", + "direction": "DEBIT", + "amountRatio": 25 + }, + { + "subject": "INCOME_TAX_PAYABLE", + "digest": "计提所得税", + "direction": "CREDIT", + "amountRatio": 25 + } + ] + } +] diff --git a/yudao-module-fms/src/main/resources/fms/closing/closing-template-presets.json b/yudao-module-fms/src/main/resources/fms/closing/closing-template-presets.json new file mode 100644 index 0000000..7e410c2 --- /dev/null +++ b/yudao-module-fms/src/main/resources/fms/closing/closing-template-presets.json @@ -0,0 +1,458 @@ +[ + { + "code": "daily-travel-reimbursement", + "name": "报销差旅费", + "category": "DAILY_EXPENSE", + "sort": 10, + "entries": [ + { + "subjectCode": "560107", + "digest": "报销差旅费", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1001", + "digest": "报销差旅费", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-telephone-expense", + "name": "报销电话费", + "category": "DAILY_EXPENSE", + "sort": 20, + "entries": [ + { + "subjectCode": "560101", + "digest": "报销电话费", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1001", + "digest": "报销电话费", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-bank-fee", + "name": "支付银行手续费", + "category": "DAILY_EXPENSE", + "sort": 30, + "entries": [ + { + "subjectCode": "560303", + "digest": "支付银行手续费", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1002", + "digest": "支付银行手续费", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-payroll", + "name": "支付工资", + "category": "DAILY_EXPENSE", + "sort": 40, + "entries": [ + { + "subjectCode": "2211", + "digest": "支付工资", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1001", + "digest": "支付工资", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-rent-utilities", + "name": "支付房租水电费", + "category": "DAILY_EXPENSE", + "sort": 50, + "entries": [ + { + "subjectCode": "560102", + "digest": "支付房租水电费", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1001", + "digest": "支付房租水电费", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-local-tax-payment", + "name": "缴纳地税款", + "category": "DAILY_EXPENSE", + "sort": 60, + "entries": [ + { + "subjectCode": "222117", + "digest": "缴纳地税款", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1002", + "digest": "缴纳地税款", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-income-tax-payment", + "name": "缴纳所得税", + "category": "DAILY_EXPENSE", + "sort": 70, + "entries": [ + { + "subjectCode": "222111", + "digest": "缴纳所得税", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1002", + "digest": "缴纳所得税", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-vat-payment", + "name": "缴纳增值税", + "category": "DAILY_EXPENSE", + "sort": 80, + "entries": [ + { + "subjectCode": "222102", + "digest": "缴纳增值税", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1002", + "digest": "缴纳增值税", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-interest-income", + "name": "利息收入", + "category": "DAILY_EXPENSE", + "sort": 90, + "entries": [ + { + "subjectCode": "1002", + "digest": "利息收入", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "560302", + "digest": "利息收入", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-cash-withdrawal", + "name": "提现金", + "category": "DAILY_EXPENSE", + "sort": 100, + "entries": [ + { + "subjectCode": "1002", + "digest": "提现金", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "560302", + "digest": "提现金", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "daily-business-entertainment", + "name": "报销业务招待费用", + "category": "DAILY_EXPENSE", + "sort": 110, + "entries": [ + { + "subjectCode": "1001", + "digest": "报销业务招待费用", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1002", + "digest": "报销业务招待费用", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "purchase-bank-payment", + "name": "采购商品(银行付款)", + "category": "PURCHASE_SALE", + "sort": 10, + "entries": [ + { + "subjectCode": "1405", + "digest": "采购商品(银行付款)", + "direction": "DEBIT", + "amountRatio": 86 + }, + { + "subjectCode": "22210101", + "digest": "采购商品(银行付款)", + "direction": "DEBIT", + "amountRatio": 14 + }, + { + "subjectCode": "1002", + "digest": "采购商品(银行付款)", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "purchase-payable", + "name": "采购商品(应付款)", + "category": "PURCHASE_SALE", + "sort": 20, + "entries": [ + { + "subjectCode": "1405", + "digest": "采购商品(应付款)", + "direction": "DEBIT", + "amountRatio": 86 + }, + { + "subjectCode": "22210101", + "digest": "采购商品(应付款)", + "direction": "DEBIT", + "amountRatio": 14 + }, + { + "subjectCode": "2202", + "digest": "采购商品(应付款)", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "sale-bank-receipt", + "name": "销售商品(银行收款)", + "category": "PURCHASE_SALE", + "sort": 30, + "entries": [ + { + "subjectCode": "1002", + "digest": "销售商品(银行收款)", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "5001", + "digest": "销售商品(银行收款)", + "direction": "CREDIT", + "amountRatio": 86 + }, + { + "subjectCode": "22210107", + "digest": "销售商品(银行收款)", + "direction": "CREDIT", + "amountRatio": 14 + } + ] + }, + { + "code": "sale-cash-receipt", + "name": "销售商品(现金收款)", + "category": "PURCHASE_SALE", + "sort": 40, + "entries": [ + { + "subjectCode": "1001", + "digest": "销售商品(现金收款)", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "5001", + "digest": "销售商品(现金收款)", + "direction": "CREDIT", + "amountRatio": 86 + }, + { + "subjectCode": "22210107", + "digest": "销售商品(现金收款)", + "direction": "CREDIT", + "amountRatio": 14 + } + ] + }, + { + "code": "sale-receivable", + "name": "销售商品(应收)", + "category": "PURCHASE_SALE", + "sort": 50, + "entries": [ + { + "subjectCode": "1122", + "digest": "销售商品(应收)", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "5001", + "digest": "销售商品(应收)", + "direction": "CREDIT", + "amountRatio": 86 + }, + { + "subjectCode": "22210107", + "digest": "销售商品(应收)", + "direction": "CREDIT", + "amountRatio": 14 + } + ] + }, + { + "code": "current-personal-loan", + "name": "个人借款", + "category": "CURRENT_ACCOUNT", + "sort": 10, + "entries": [ + { + "subjectCode": "1221", + "digest": "个人借款", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1001", + "digest": "个人借款", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "current-personal-repayment", + "name": "个人还款", + "category": "CURRENT_ACCOUNT", + "sort": 20, + "entries": [ + { + "subjectCode": "1001", + "digest": "个人还款", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1221", + "digest": "个人还款", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "current-receive-payment", + "name": "收货款", + "category": "CURRENT_ACCOUNT", + "sort": 30, + "entries": [ + { + "subjectCode": "1002", + "digest": "收货款", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1122", + "digest": "收货款", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "current-pay-supplier", + "name": "支付货款", + "category": "CURRENT_ACCOUNT", + "sort": 40, + "entries": [ + { + "subjectCode": "2202", + "digest": "支付货款", + "direction": "DEBIT", + "amountRatio": 100 + }, + { + "subjectCode": "1002", + "digest": "支付货款", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + }, + { + "code": "transfer-accrue-payroll", + "name": "计提工资", + "category": "TRANSFER_BUSINESS", + "sort": 10, + "entries": [ + { + "subjectCode": "560110", + "digest": "计提工资", + "direction": "DEBIT", + "amountRatio": 15 + }, + { + "subjectCode": "560209", + "digest": "计提工资", + "direction": "DEBIT", + "amountRatio": 85 + }, + { + "subjectCode": "2211", + "digest": "计提工资", + "direction": "CREDIT", + "amountRatio": 100 + } + ] + } +] diff --git a/yudao-module-fms/src/main/resources/fms/config/finance-indicator-presets.json b/yudao-module-fms/src/main/resources/fms/config/finance-indicator-presets.json new file mode 100644 index 0000000..c5c63b3 --- /dev/null +++ b/yudao-module-fms/src/main/resources/fms/config/finance-indicator-presets.json @@ -0,0 +1,114 @@ +[ + { + "code": "income", + "name": "收入", + "type": 2, + "formula": "L1", + "sort": 10 + }, + { + "code": "operatingCost", + "name": "成本", + "type": 2, + "formula": "L2+L3", + "sort": 20 + }, + { + "code": "profit", + "name": "利润", + "type": 2, + "formula": "L30", + "sort": 30 + }, + { + "code": "expense", + "name": "费用", + "type": 2, + "formula": "L11+L14+L18", + "sort": 40 + }, + { + "code": "other", + "name": "其他", + "type": 2, + "formula": "L30+L11+L14+L18+L2+L3-L1", + "sort": 50 + }, + { + "code": "cash", + "name": "现金", + "type": 1, + "formula": "[{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"1001\"},{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"1012\"}]", + "sort": 60 + }, + { + "code": "bank", + "name": "银行存款", + "type": 1, + "formula": "[{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"1002\"}]", + "sort": 70 + }, + { + "code": "inventory", + "name": "存货", + "type": 1, + "formula": "L9", + "sort": 80 + }, + { + "code": "liabilityTotal", + "name": "负债总额", + "type": 1, + "formula": "L47", + "sort": 90 + }, + { + "code": "loan", + "name": "贷款总额", + "type": 1, + "formula": "L31+L42", + "sort": 100 + }, + { + "code": "receivable", + "name": "应收账款", + "type": 1, + "formula": "[{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"1122\"}]", + "sort": 110 + }, + { + "code": "payable", + "name": "应付账款", + "type": 1, + "formula": "[{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"2202\"}]", + "sort": 120 + }, + { + "code": "taxPayable", + "name": "应交税费", + "type": 1, + "formula": "[{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"2221\"}]", + "sort": 130 + }, + { + "code": "mainIncome", + "name": "主营业务收入", + "type": 2, + "formula": "[{\"operator\":\"+\",\"rules\":6,\"subjectNumber\":\"5001\"}]", + "sort": 140 + }, + { + "code": "otherIncome", + "name": "营业外收入", + "type": 2, + "formula": "[{\"operator\":\"+\",\"rules\":6,\"subjectNumber\":\"5301\"}]", + "sort": 150 + }, + { + "code": "fixedAssets", + "name": "固定资产", + "type": 1, + "formula": "L20", + "sort": 160 + } +] diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerMultiColumnExcelHelperTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerMultiColumnExcelHelperTest.java new file mode 100644 index 0000000..20bd945 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/FmsLedgerMultiColumnExcelHelperTest.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.fms.controller.admin.ledger; + +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.multicolumn.FmsLedgerMultiColumnRespVO; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FmsLedgerMultiColumnExcelHelperTest { + + @Test + public void testBuildHeadAndData() { + // mock 数据 + FmsLedgerMultiColumnRespVO.Column debitColumn = new FmsLedgerMultiColumnRespVO.Column() + .setSubjectId(101L).setSubjectCode("660201").setSubjectName("办公费") + .setBalanceDirection(FmsDebitCreditDirectionEnum.DEBIT.getType()); + FmsLedgerMultiColumnRespVO.Column creditColumn = new FmsLedgerMultiColumnRespVO.Column() + .setSubjectId(102L).setSubjectCode("2202").setSubjectName("应付账款") + .setBalanceDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + Map columnAmounts = new LinkedHashMap<>(); + columnAmounts.put(debitColumn.getSubjectId(), new BigDecimal("80.00")); + columnAmounts.put(creditColumn.getSubjectId(), new BigDecimal("30.00")); + FmsLedgerDetailRespVO row = new FmsLedgerDetailRespVO() + .setAccountDate(LocalDate.of(2026, 8, 31)).setVoucherNumber("记-1") + .setDigest("期末余额").setDebitAmount(BigDecimal.ZERO).setCreditAmount(BigDecimal.ZERO) + .setBalanceDirection("借").setBalance(new BigDecimal("50.00")) + .setColumnAmounts(columnAmounts); + FmsLedgerMultiColumnRespVO result = new FmsLedgerMultiColumnRespVO() + .setColumns(Arrays.asList(creditColumn, debitColumn)) + .setRows(Collections.singletonList(row)); + + // 调用 + List> head = FmsLedgerMultiColumnExcelHelper.buildHead(result); + List> data = FmsLedgerMultiColumnExcelHelper.buildData(result); + + // 断言 + assertEquals(9, head.size()); + assertEquals(Arrays.asList("借", "660201/办公费"), head.get(7)); + assertEquals(Arrays.asList("贷", "2202/应付账款"), head.get(8)); + assertEquals(1, data.size()); + assertEquals(new BigDecimal("80.00"), data.get(0).get(7)); + assertEquals(new BigDecimal("30.00"), data.get(0).get(8)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodServiceImplTest.java new file mode 100644 index 0000000..0e1f653 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingPeriodServiceImplTest.java @@ -0,0 +1,428 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.income.FmsIncomeStatementCheckRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingPeriodDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingPeriodMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherStatusEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.report.FmsBalanceSheetService; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.Arrays; +import java.util.Collections; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +@Import(FmsClosingPeriodServiceImpl.class) +public class FmsClosingPeriodServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsClosingPeriodServiceImpl closingPeriodService; + @Resource + private FmsClosingPeriodMapper closingPeriodMapper; + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + @MockitoBean + private FmsClosingVoucherService closingVoucherService; + @MockitoBean + private FmsFinanceParameterService financeParameterService; + @MockitoBean + private FmsInitialBalanceService initialBalanceService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsLedgerService ledgerService; + @MockitoBean + private FmsVoucherService voucherService; + @MockitoBean + private FmsBalanceSheetService balanceSheetService; + @MockitoBean + private FmsIncomeStatementService incomeStatementService; + + @Test + public void testGetCurrentMonth_noClosingPeriod() { + // 准备参数 + LocalDateTime startTime = LocalDateTime.of(2026, 3, 1, 0, 0); + + // 调用,并断言 + assertEquals(YearMonth.of(2026, 3), closingPeriodService.getCurrentMonth(1L, startTime)); + } + + @Test + public void testGetCurrentMonth_hasClosingPeriod() { + // mock 数据 + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 6, 30, 0, 0)).setAccountSetId(1L)); + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 7, 31, 0, 0)).setAccountSetId(1L)); + + // 调用,并断言 + assertEquals(YearMonth.of(2026, 8), closingPeriodService.getCurrentMonth( + 1L, LocalDateTime.of(2026, 3, 1, 0, 0))); + } + + @Test + public void testGetCurrentMonth() { + // mock 数据 + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn( + new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + closingPeriodMapper.insert(buildClosingPeriod(1L, YearMonth.of(2026, 8))); + + // 调用,并断言 + assertEquals("2026-08", closingPeriodService.getCurrentMonth(1L, 10L)); + } + + @Test + public void testGetClosingOverview_excludeClosingVoucherFromPendingCount() { + // mock 数据 + when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn( + new FmsAccountSetDO().setId(1L).setInitialized(true)); + when(voucherService.getVoucherListByPeriod(eq(1L), any(), any())).thenReturn(Arrays.asList( + new FmsVoucherDO().setId(11L).setVoucherWordId(1L).setVoucherNumber(1) + .setStatus(FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()), + new FmsVoucherDO().setId(12L).setVoucherWordId(1L).setVoucherNumber(2) + .setStatus(FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()), + new FmsVoucherDO().setId(13L).setVoucherWordId(1L).setVoucherNumber(3) + .setStatus(FmsVoucherStatusEnum.APPROVED.getStatus()))); + when(closingVoucherService.getClosingVoucherListByPeriod(eq(1L), any(), any())) + .thenReturn(Collections.singletonList(new FmsClosingVoucherDO().setVoucherId(12L))); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(Collections.emptyList()); + when(balanceSheetService.getBalanceSheet(any(), eq(10L))).thenReturn(Collections.emptyList()); + when(financeParameterService.getFinanceParameter(1L, 10L)) + .thenReturn(new FmsFinanceParameterDO().setVoucherReviewRequired(true)); + + // 调用 + FmsClosingOverviewRespVO overview = closingPeriodService.getClosingOverview( + buildClosingQueryReqVO(), 10L); + + // 断言 + assertEquals(1L, overview.getPendingVoucherCount()); + assertEquals(3L, overview.getVoucherCount()); + } + + @Test + public void testGetClosingOverview_allChecksPassed() { + // mock 数据 + mockClosePeriodChecks(); + + // 调用 + FmsClosingOverviewRespVO overview = closingPeriodService.getClosingOverview( + buildClosingQueryReqVO(), 10L); + + // 断言 + assertEquals(true, overview.getInitialBalanceBalanced()); + assertEquals(true, overview.getVoucherNumberContinuous()); + assertEquals(true, overview.getProfitLossVoucherGenerated()); + assertEquals(true, overview.getIncomeStatementBalanced()); + assertEquals(0, overview.getIncomeStatementUnmappedSubjectCount()); + assertEquals(true, overview.getBalanceSheetProfitLossTransferred()); + assertEquals(true, overview.getBalanceSheetBalanced()); + assertEquals(0, overview.getBalanceSheetUnmappedSubjectCount()); + assertEquals(true, overview.getCanClose()); + } + + @Test + public void testClosePeriod_notCurrentPeriod() { + // mock 数据 + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn( + new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + closingPeriodMapper.insert(buildClosingPeriod(1L, YearMonth.of(2026, 8))); + + // 准备参数 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-07"); + + // 调用,并断言异常 + assertServiceException(() -> closingPeriodService.closePeriod(reqVO, 10L), + CLOSING_NOT_CURRENT_PERIOD); + } + + @Test + public void testClosePeriod() { + // mock 数据 + mockClosePeriodChecks(); + + // 准备参数 + FmsClosingQueryReqVO reqVO = buildClosingQueryReqVO(); + + // 调用 + closingPeriodService.closePeriod(reqVO, 10L); + + // 断言 + assertNotNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTime.of(2026, 8, 31, 23, 59, 59))); + verify(closingVoucherService).updateClosingVoucherClosedByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 8, 1, 0, 0)), true); + } + + @Test + public void testClosePeriod_toTargetMonth() { + // mock 数据 + mockClosePeriodChecks(); + + // 准备参数:当前期间为 2026-08,结账目标为 2026-09 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO() + .setAccountSetId(1L).setMonth("2026-09"); + + // 调用 + closingPeriodService.closePeriod(reqVO, 10L); + + // 断言:目标期间之前的每个月都已登记结账 + assertNotNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTimeUtils.endOfMonth( + LocalDateTime.of(2026, 8, 1, 0, 0)))); + assertNotNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 9, 1, 0, 0), LocalDateTimeUtils.endOfMonth( + LocalDateTime.of(2026, 9, 1, 0, 0)))); + verify(closingVoucherService).updateClosingVoucherClosedByPeriod(1L, + LocalDateTime.of(2026, 9, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 9, 1, 0, 0)), true); + } + + @Test + public void testClosePeriod_initialBalanceUnbalanced() { + // mock 数据 + mockClosePeriodChecks(); + when(initialBalanceService.getTrialBalance(1L, 10L)) + .thenReturn(new FmsTrialBalanceRespVO().setBalanced(false)); + + // 调用,并断言异常 + assertServiceException(() -> closingPeriodService.closePeriod(buildClosingQueryReqVO(), 10L), + CLOSING_INITIAL_BALANCE_UNBALANCED); + assertPeriodNotClosed(); + } + + @Test + public void testClosePeriod_voucherNumberDiscontinuous() { + // mock 数据 + mockClosePeriodChecks(); + when(voucherService.getVoucherListByPeriod(eq(1L), any(), any())).thenReturn(java.util.Arrays.asList( + new FmsVoucherDO().setVoucherWordId(1L).setVoucherNumber(1), + new FmsVoucherDO().setVoucherWordId(1L).setVoucherNumber(3))); + + // 调用,并断言异常 + assertServiceException(() -> closingPeriodService.closePeriod(buildClosingQueryReqVO(), 10L), + CLOSING_VOUCHER_NUMBER_DISCONTINUOUS); + assertPeriodNotClosed(); + } + + @Test + public void testClosePeriod_profitLossActivityWithoutClosingVoucher() { + // mock 数据 + mockClosePeriodChecks(); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Collections.singletonList( + new FmsSubjectDO().setId(1L).setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()))); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(Collections.singletonList( + new FmsLedgerSubjectBalanceRespVO().setSubjectId(1L) + .setPeriodDebitAmount(new BigDecimal("100.00")) + .setPeriodCreditAmount(new BigDecimal("100.00")))); + + // 调用,并断言异常:损益净额为零,也不能绕过结转凭证检查 + assertServiceException(() -> closingPeriodService.closePeriod(buildClosingQueryReqVO(), 10L), + CLOSING_PROFIT_LOSS_VOUCHER_MISSING); + assertPeriodNotClosed(); + } + + @Test + public void testClosePeriod_incomeStatementUnmappedSubject() { + // mock 数据 + mockClosePeriodChecks(); + when(incomeStatementService.checkIncomeStatement(any(), eq(10L))) + .thenReturn(new FmsIncomeStatementCheckRespVO().setBalanced(true) + .setUnmappedSubjects(Collections.singletonList( + new FmsIncomeStatementCheckRespVO.UnmappedSubject().setId(1L)))); + + // 调用,并断言异常 + assertServiceException(() -> closingPeriodService.closePeriod(buildClosingQueryReqVO(), 10L), + CLOSING_REPORT_SUBJECT_UNMAPPED); + assertPeriodNotClosed(); + } + + @Test + public void testClosePeriod_balanceSheetUnbalanced() { + // mock 数据 + mockClosePeriodChecks(); + when(balanceSheetService.checkBalanceSheet(any(), eq(10L))) + .thenReturn(new FmsBalanceSheetCheckRespVO().setBalanced(false) + .setInitialBalanceBalanced(true).setProfitLossTransferred(true) + .setUnmappedSubjects(Collections.emptyList())); + + // 调用,并断言异常 + assertServiceException(() -> closingPeriodService.closePeriod(buildClosingQueryReqVO(), 10L), + CLOSING_BALANCE_SHEET_UNBALANCED); + assertPeriodNotClosed(); + } + + @Test + public void testCancelClosePeriod() { + // mock 数据 + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true)); + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 7, 31, 0, 0)).setAccountSetId(1L)); + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 8, 31, 0, 0)).setAccountSetId(1L)); + // 准备参数 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + + // 调用 + closingPeriodService.cancelClosePeriod(reqVO, 10L); + + // 断言 + assertNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTime.of(2026, 8, 31, 23, 59, 59))); + assertEquals(YearMonth.of(2026, 7), YearMonth.from(closingPeriodMapper + .selectLatestByAccountSetId(1L).getClosingTime())); + verify(closingVoucherService).updateClosingVoucherClosedByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 8, 1, 0, 0)), false); + } + + @Test + public void testCancelClosePeriod_targetAfterLatestPeriod() { + // mock 数据 + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true)); + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 7, 31, 0, 0)).setAccountSetId(1L)); + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 8, 31, 0, 0)).setAccountSetId(1L)); + + // 准备参数 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-09"); + + // 调用,并断言异常 + assertServiceException(() -> closingPeriodService.cancelClosePeriod(reqVO, 10L), + CLOSING_PERIOD_NOT_CLOSED); + assertNotNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 7, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 7, 1, 0, 0)))); + assertNotNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 8, 1, 0, 0)))); + verify(closingVoucherService, never()).updateClosingVoucherClosedByPeriod( + any(), any(), any(), eq(false)); + } + + @Test + public void testCancelClosePeriod_toTargetMonth() { + // mock 数据 + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true) + .setStartTime(LocalDateTime.of(2026, 7, 1, 0, 0))); + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 7, 31, 0, 0)).setAccountSetId(1L)); + closingPeriodMapper.insert(new FmsClosingPeriodDO() + .setClosingTime(LocalDateTime.of(2026, 8, 31, 0, 0)).setAccountSetId(1L)); + + // 准备参数:从最近期间反结账到 2026-07 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO() + .setAccountSetId(1L).setMonth("2026-07"); + + // 调用 + closingPeriodService.cancelClosePeriod(reqVO, 10L); + + // 断言:目标期间及之后的结账均已撤销 + assertNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 7, 1, 0, 0), LocalDateTimeUtils.endOfMonth( + LocalDateTime.of(2026, 7, 1, 0, 0)))); + assertNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTimeUtils.endOfMonth( + LocalDateTime.of(2026, 8, 1, 0, 0)))); + verify(closingVoucherService).updateClosingVoucherClosedByPeriod(1L, + LocalDateTime.of(2026, 7, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 7, 1, 0, 0)), false); + verify(closingVoucherService).updateClosingVoucherClosedByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 8, 1, 0, 0)), false); + } + + // ========== 随机对象 ========== + + private FmsClosingPeriodDO buildClosingPeriod(Long accountSetId, YearMonth currentMonth) { + YearMonth previousMonth = currentMonth.minusMonths(1); + return new FmsClosingPeriodDO().setAccountSetId(accountSetId) + .setClosingTime(previousMonth.atEndOfMonth().atStartOfDay()); + } + + private FmsClosingQueryReqVO buildClosingQueryReqVO() { + return new FmsClosingQueryReqVO().setAccountSetId(1L).setMonth("2026-08"); + } + + private void mockClosePeriodChecks() { + LocalDateTime startTime = LocalDateTime.of(2026, 8, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(new FmsAccountSetDO().setId(1L) + .setInitialized(true).setStartTime(startTime)); + when(financeParameterService.getFinanceParameter(1L, 10L)) + .thenReturn(new FmsFinanceParameterDO().setVoucherReviewRequired(false)); + closingPeriodMapper.insert(buildClosingPeriod(1L, YearMonth.of(2026, 8))); + when(voucherService.getVoucherListByPeriod(eq(1L), any(), any())).thenReturn(Collections.emptyList()); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(Collections.emptyList()); + when(balanceSheetService.getBalanceSheet(any(), eq(10L))).thenReturn(Collections.emptyList()); + when(initialBalanceService.getTrialBalance(1L, 10L)) + .thenReturn(new FmsTrialBalanceRespVO().setBalanced(true)); + when(incomeStatementService.checkIncomeStatement(any(), eq(10L))) + .thenReturn(new FmsIncomeStatementCheckRespVO().setBalanced(true) + .setUnmappedSubjects(Collections.emptyList())); + when(balanceSheetService.checkBalanceSheet(any(), eq(10L))) + .thenReturn(new FmsBalanceSheetCheckRespVO().setBalanced(true) + .setInitialBalanceBalanced(true).setProfitLossTransferred(true) + .setUnmappedSubjects(Collections.emptyList())); + } + + private void assertPeriodNotClosed() { + assertNull(closingPeriodMapper.selectByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTimeUtils.endOfMonth(LocalDateTime.of(2026, 8, 1, 0, 0)))); + verify(closingVoucherService, never()).updateClosingVoucherClosedByPeriod(any(), any(), any(), eq(true)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeServiceImplTest.java new file mode 100644 index 0000000..86a24a0 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingSchemeServiceImplTest.java @@ -0,0 +1,467 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossSettingsSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsSpecialClosingSettingsSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingSchemeMapper; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingVoucherTypeEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_SCHEME_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_SCHEME_RATIO_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsClosingSchemeServiceImpl.class) +public class FmsClosingSchemeServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsClosingSchemeServiceImpl closingSchemeService; + @Resource + private FmsClosingSchemeMapper closingSchemeMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsClosingVoucherService closingVoucherService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsFinanceParameterService financeParameterService; + @MockitoBean + private FmsLedgerService ledgerService; + @MockitoBean + private FmsVoucherWordService voucherWordService; + @MockitoBean + private FmsIncomeStatementService incomeStatementService; + + @BeforeEach + public void before() { + when(financeParameterService.getFinanceParameter(1L)).thenReturn(new FmsFinanceParameterDO() + .setSubjectCodeRule(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE)); + when(financeParameterService.convertStandardSubjectCode(any(), any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + public void testGetClosingSchemeCountByVoucherWordId() { + // mock 数据 + closingSchemeMapper.insert(new FmsClosingSchemeDO().setName("结转损益") + .setAccountSetId(1L).setVoucherWordId(11L)); + closingSchemeMapper.insert(new FmsClosingSchemeDO().setName("结转损益") + .setAccountSetId(2L).setVoucherWordId(11L)); + + // 调用,并断言 + assertEquals(1L, closingSchemeService.getClosingSchemeCountByVoucherWordId(1L, 11L)); + assertEquals(0L, closingSchemeService.getClosingSchemeCountByVoucherWordId(1L, 12L)); + } + + @Test + public void testSaveProfitLossSettings() { + // mock 数据 + FmsSubjectDO adjustmentSubject = new FmsSubjectDO().setId(101L).setCode("6000") + .setName("以前年度损益调整").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO adjustmentClosingSubject = new FmsSubjectDO().setId(102L).setCode("3104") + .setName("利润分配").setType(FmsSubjectTypeEnum.EQUITY.getType()); + FmsSubjectDO otherClosingSubject = new FmsSubjectDO().setId(103L).setCode("3103") + .setName("本年利润").setType(FmsSubjectTypeEnum.EQUITY.getType()); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + adjustmentSubject, adjustmentClosingSubject, otherClosingSubject)); + + // 准备参数 + FmsProfitLossSettingsSaveReqVO reqVO = new FmsProfitLossSettingsSaveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setVoucherWordId(11L); + reqVO.setDigest("期末结转损益"); + reqVO.setVoucherType(FmsClosingVoucherTypeEnum.SEPARATE_GAIN_AND_LOSS.getType()); + reqVO.setPriorYearAdjustmentSubjectId(101L); + reqVO.setAdjustmentClosingSubjectId(102L); + reqVO.setOtherClosingSubjectId(103L); + reqVO.setReverseBalance(true); + reqVO.setClosingDay(28); + + // 调用 + Long id = closingSchemeService.saveProfitLossSettings(reqVO, 10L); + + // 断言 + FmsClosingSchemeDO closing = closingSchemeMapper.selectById(id); + assertEquals("期末结转损益", closing.getDigest()); + assertEquals(FmsClosingVoucherTypeEnum.SEPARATE_GAIN_AND_LOSS.getType(), closing.getVoucherType()); + assertEquals(101L, closing.getPriorYearAdjustmentSubjectId()); + assertEquals(102L, closing.getAdjustmentClosingSubjectId()); + assertEquals(103L, closing.getOtherClosingSubjectId()); + assertEquals(28, closing.getClosingDay()); + verify(voucherWordService).validateVoucherWordExists(1L, 11L); + } + + @Test + public void testCreateClosingScheme() { + // mock 数据 + FmsSubjectDO sourceSubject = new FmsSubjectDO().setId(101L).setCode("6602") + .setName("管理费用"); + FmsSubjectDO debitSubject = new FmsSubjectDO().setId(102L).setCode("660201") + .setName("房租"); + FmsSubjectDO creditSubject = new FmsSubjectDO().setId(103L).setCode("2701") + .setName("长期待摊费用"); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Arrays.asList(sourceSubject, debitSubject, creditSubject)); + + // 准备参数 + FmsClosingSchemeSaveReqVO reqVO = buildClosingSchemeSaveReqVO(); + + // 调用 + Long id = closingSchemeService.createClosingScheme(reqVO, 10L); + + // 断言 + FmsClosingSchemeDO closing = closingSchemeMapper.selectById(id); + assertEquals("结转本月房租", closing.getName()); + assertEquals(FmsClosingTypeEnum.REGULAR.getType(), closing.getType()); + assertEquals(2, closing.getSubjectRules().size()); + assertEquals("660201", closing.getSubjectRules().get(0).getSubjectCode()); + assertEquals("2701", closing.getSubjectRules().get(1).getSubjectCode()); + } + + @Test + public void testUpdateClosingScheme_ratioInvalid() { + // mock 数据 + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setName("原结转方案") + .setAccountSetId(1L).setType(FmsClosingTypeEnum.REGULAR.getType()); + closingSchemeMapper.insert(closing); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setCode("6602"), + new FmsSubjectDO().setId(102L).setCode("660201"), + new FmsSubjectDO().setId(103L).setCode("2701"))); + + // 准备参数 + FmsClosingSchemeSaveReqVO reqVO = buildClosingSchemeSaveReqVO(); + reqVO.setId(closing.getId()); + reqVO.setName("非法比例方案"); + reqVO.getSubjects().get(0).setAmountRatio(new BigDecimal("99")); + + // 调用,并断言异常 + assertServiceException(() -> closingSchemeService.updateClosingScheme(reqVO, 10L), + CLOSING_SCHEME_RATIO_INVALID); + assertEquals("原结转方案", closingSchemeMapper.selectById(closing.getId()).getName()); + } + + @Test + public void testDeleteClosingScheme_inUse() { + // mock 数据 + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setName("已生成凭证方案") + .setAccountSetId(1L).setType(FmsClosingTypeEnum.REGULAR.getType()); + closingSchemeMapper.insert(closing); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(closingVoucherService.getClosingVoucherCountByClosingId(closing.getId())).thenReturn(1L); + + // 调用,并断言异常 + assertServiceException(() -> closingSchemeService.deleteClosingScheme(1L, closing.getId(), 10L), + CLOSING_SCHEME_IN_USE); + assertNotNull(closingSchemeMapper.selectById(closing.getId())); + } + + @Test + public void testUpdateSpecialClosingSettings() { + // mock 数据 + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setName("计提附加税") + .setAccountSetId(1L).setVoucherWordId(11L) + .setType(FmsClosingTypeEnum.LOCAL_TAX.getType()); + closingSchemeMapper.insert(closing); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setCode("540303"), + new FmsSubjectDO().setId(102L).setCode("222113"))); + + // 准备参数 + FmsSpecialClosingSettingsSaveReqVO reqVO = new FmsSpecialClosingSettingsSaveReqVO(); + reqVO.setId(closing.getId()); + reqVO.setAccountSetId(1L); + reqVO.setVoucherWordId(12L); + FmsSpecialClosingSettingsSaveReqVO.SubjectRule debitRule = + new FmsSpecialClosingSettingsSaveReqVO.SubjectRule(); + debitRule.setSubjectId(101L); + debitRule.setDigest("计提附加税"); + debitRule.setDirection(FmsDebitCreditDirectionEnum.DEBIT.getType()); + debitRule.setAmountRatio(new BigDecimal("5")); + FmsSpecialClosingSettingsSaveReqVO.SubjectRule creditRule = + new FmsSpecialClosingSettingsSaveReqVO.SubjectRule(); + creditRule.setSubjectId(102L); + creditRule.setDigest("计提附加税"); + creditRule.setDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + creditRule.setAmountRatio(new BigDecimal("5")); + reqVO.setSubjects(Arrays.asList(debitRule, creditRule)); + + // 调用 + closingSchemeService.updateSpecialClosingSettings(reqVO, 10L); + + // 断言 + FmsClosingSchemeDO actual = closingSchemeMapper.selectById(closing.getId()); + assertEquals(12L, actual.getVoucherWordId()); + assertEquals("540303", actual.getSubjectRules().get(0).getSubjectCode()); + assertEquals("222113", actual.getSubjectRules().get(1).getSubjectCode()); + verify(voucherWordService).validateVoucherWordExists(1L, 12L); + } + + @Test + public void testInitializeDefaultClosingSchemes() { + // mock 数据 + List subjects = buildSpecialClosingSubjects(); + subjects.add(new FmsSubjectDO().setId(401L).setCode("6000")); + subjects.add(new FmsSubjectDO().setId(402L).setCode("310415")); + subjects.add(new FmsSubjectDO().setId(403L).setCode("3103")); + subjects.add(new FmsSubjectDO().setId(404L).setCode("222101")); + subjects.add(new FmsSubjectDO().setId(405L).setCode("222121")); + when(voucherWordService.getVoucherWordList(1L)).thenReturn(Collections.singletonList( + new FmsVoucherWordDO().setId(11L).setDefaultStatus(true))); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(subjects); + + // 调用 + closingSchemeService.initializeDefaultClosingSchemes(1L, 10L); + closingSchemeService.initializeDefaultClosingSchemes(1L, 10L); + + // 断言 + List closings = closingSchemeMapper.selectListByAccountSetId(1L); + assertEquals(4, closings.size()); + FmsClosingSchemeDO profitLossClosing = CollUtil.findOne(closings, + item -> item.getType().equals(FmsClosingTypeEnum.PROFIT_LOSS.getType())); + assertNotNull(profitLossClosing); + assertEquals(11L, profitLossClosing.getVoucherWordId()); + assertEquals(401L, profitLossClosing.getPriorYearAdjustmentSubjectId()); + assertEquals(402L, profitLossClosing.getAdjustmentClosingSubjectId()); + assertEquals(403L, profitLossClosing.getOtherClosingSubjectId()); + assertEquals(FmsClosingTypeEnum.PROFIT_LOSS.getDigest(), profitLossClosing.getDigest()); + assertEquals(FmsClosingVoucherTypeEnum.COMBINED_GAIN_AND_LOSS.getType(), + profitLossClosing.getVoucherType()); + assertEquals(FmsClosingTypeEnum.UNPAID_VAT.getDigest(), CollUtil.findOne(closings, + item -> item.getType().equals(FmsClosingTypeEnum.UNPAID_VAT.getType())).getDigest()); + assertEquals(FmsClosingTypeEnum.LOCAL_TAX.getDigest(), CollUtil.findOne(closings, + item -> item.getType().equals(FmsClosingTypeEnum.LOCAL_TAX.getType())).getDigest()); + assertEquals(FmsClosingTypeEnum.INCOME_TAX.getDigest(), CollUtil.findOne(closings, + item -> item.getType().equals(FmsClosingTypeEnum.INCOME_TAX.getType())).getDigest()); + } + + @Test + public void testGetClosingSchemeList_notInitializeOnQuery() { + // mock 数据 + List subjects = buildSpecialClosingSubjects(); + FmsSubjectDO vatSubject = new FmsSubjectDO().setId(201L).setCode("222101") + .setName("应交增值税"); + FmsSubjectDO consumptionTaxSubject = new FmsSubjectDO().setId(202L).setCode("222121") + .setName("应交消费税"); + subjects.add(vatSubject); + subjects.add(consumptionTaxSubject); + subjects.add(new FmsSubjectDO().setId(203L).setCode("6000")); + subjects.add(new FmsSubjectDO().setId(204L).setCode("310415")); + subjects.add(new FmsSubjectDO().setId(205L).setCode("3103")); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(voucherWordService.getVoucherWordList(1L)).thenReturn(Collections.singletonList( + new FmsVoucherWordDO().setId(11L).setDefaultStatus(true))); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(subjects); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(Arrays.asList( + new FmsLedgerSubjectBalanceRespVO().setSubjectId(201L) + .setEndingDebitAmount(BigDecimal.ZERO).setEndingCreditAmount(new BigDecimal("1000.00")) + .setChildren(Collections.emptyList()), + new FmsLedgerSubjectBalanceRespVO().setSubjectId(202L) + .setEndingDebitAmount(BigDecimal.ZERO).setEndingCreditAmount(new BigDecimal("500.00")) + .setChildren(Collections.emptyList()))); + when(incomeStatementService.getIncomeStatement(any(), eq(10L))).thenReturn(Arrays.asList( + new FmsReportItemRespVO().setRowNo(30).setYearAmount(new BigDecimal("10000.00")), + new FmsReportItemRespVO().setRowNo(30).setYearAmount(new BigDecimal("20000.00")), + new FmsReportItemRespVO().setRowNo(31).setYearAmount(new BigDecimal("1000.00")))); + closingSchemeService.initializeDefaultClosingSchemes(1L, 10L); + Long closingCount = closingSchemeMapper.selectCount(); + + // 准备参数 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + + // 调用 + List results = closingSchemeService.getClosingSchemeList(reqVO, 10L); + + // 断言 + assertEquals(4, results.size()); + FmsClosingSchemeRespVO unpaidVatClosing = CollUtil.findOne(results, + item -> item.getType().equals(FmsClosingTypeEnum.UNPAID_VAT.getType())); + FmsClosingSchemeRespVO localTaxClosing = CollUtil.findOne(results, + item -> item.getType().equals(FmsClosingTypeEnum.LOCAL_TAX.getType())); + FmsClosingSchemeRespVO incomeTaxClosing = CollUtil.findOne(results, + item -> item.getType().equals(FmsClosingTypeEnum.INCOME_TAX.getType())); + assertEquals(new BigDecimal("1000.00"), unpaidVatClosing.getBalance()); + assertEquals(new BigDecimal("180.00"), localTaxClosing.getBalance()); + assertEquals(new BigDecimal("1500.00"), incomeTaxClosing.getBalance()); + assertEquals(6, localTaxClosing.getSubjects().size()); + assertEquals(new BigDecimal("25"), incomeTaxClosing.getSubjects().get(0).getAmountRatio()); + assertEquals(closingCount, closingSchemeMapper.selectCount()); + } + + @Test + public void testGetClosingSchemeList_yearBeginBalance() { + // mock 数据 + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setName("年初余额结转") + .setAccountSetId(1L).setType(FmsClosingTypeEnum.REGULAR.getType()).setPeriodEnd(true) + .setSubjectId(101L).setFormulaRule(FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + .setTimeType(FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()); + closingSchemeMapper.insert(closing); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Collections.singletonList( + new FmsSubjectDO().setId(101L).setCode("1001"))); + FmsLedgerSubjectBalanceRespVO currentBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setOpeningDebitAmount(new BigDecimal("150.00")).setOpeningCreditAmount(BigDecimal.ZERO) + .setEndingDebitAmount(new BigDecimal("160.00")).setEndingCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()); + FmsLedgerSubjectBalanceRespVO yearBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setOpeningDebitAmount(new BigDecimal("100.00")).setOpeningCreditAmount(BigDecimal.ZERO) + .setEndingDebitAmount(new BigDecimal("160.00")).setEndingCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(currentBalance), Collections.singletonList(yearBalance)); + when(closingVoucherService.getClosingVoucherListByPeriod(eq(1L), any(), any())) + .thenReturn(Collections.emptyList()); + + // 准备参数 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + + // 调用 + List results = closingSchemeService.getClosingSchemeList(reqVO, 10L); + + // 断言 + assertEquals(1, results.size()); + assertEquals(new BigDecimal("100.00"), results.get(0).getBalance()); + } + + @Test + public void testGetClosingSchemeList_sumGeneratedVoucherAmounts() { + // mock 数据 + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setName("结转损益") + .setAccountSetId(1L).setType(FmsClosingTypeEnum.PROFIT_LOSS.getType()).setPeriodEnd(true); + closingSchemeMapper.insert(closing); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(Collections.emptyList()); + when(closingVoucherService.getClosingVoucherListByPeriod(eq(1L), any(), any())) + .thenReturn(Arrays.asList( + new FmsClosingVoucherDO().setClosingId(closing.getId()).setVoucherId(801L) + .setAmount(new BigDecimal("400.00")), + new FmsClosingVoucherDO().setClosingId(closing.getId()).setVoucherId(802L) + .setAmount(new BigDecimal("600.00")))); + + // 调用 + List results = closingSchemeService.getClosingSchemeList( + new FmsClosingQueryReqVO().setAccountSetId(1L).setMonth("2026-08"), 10L); + + // 断言 + assertEquals(1, results.size()); + assertEquals(new BigDecimal("1000.00"), results.get(0).getBalance()); + assertEquals(Arrays.asList(801L, 802L), results.get(0).getVoucherIds()); + } + + @Test + public void testGetClosingSchemeList_filterNonPeriodEndRegularScheme() { + // mock 数据 + closingSchemeMapper.insert(new FmsClosingSchemeDO().setName("非期末自动结转方案") + .setAccountSetId(1L).setType(FmsClosingTypeEnum.REGULAR.getType()).setPeriodEnd(false)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + // 准备参数 + FmsClosingQueryReqVO reqVO = new FmsClosingQueryReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + + // 调用 + List results = closingSchemeService.getClosingSchemeList(reqVO, 10L); + + // 断言 + assertEquals(0, results.size()); + } + + // ========== 随机对象 ========== + + private FmsClosingSchemeSaveReqVO buildClosingSchemeSaveReqVO() { + FmsClosingSchemeSaveReqVO reqVO = new FmsClosingSchemeSaveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setName("结转本月房租"); + reqVO.setPeriodEnd(true); + reqVO.setSubjectId(101L); + reqVO.setFormulaRule(FmsFormulaRuleEnum.BALANCE.getRule()); + reqVO.setTimeType(FmsClosingTimeTypeEnum.PERIOD_END.getType()); + reqVO.setVoucherWordId(11L); + FmsClosingSchemeSaveReqVO.SubjectRule debitRule = new FmsClosingSchemeSaveReqVO.SubjectRule(); + debitRule.setSubjectId(102L); + debitRule.setDigest("结转本月房租"); + debitRule.setDirection(FmsDebitCreditDirectionEnum.DEBIT.getType()); + debitRule.setAmountRatio(new BigDecimal("100")); + FmsClosingSchemeSaveReqVO.SubjectRule creditRule = new FmsClosingSchemeSaveReqVO.SubjectRule(); + creditRule.setSubjectId(103L); + creditRule.setDigest("结转本月房租"); + creditRule.setDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + creditRule.setAmountRatio(new BigDecimal("100")); + reqVO.setSubjects(Arrays.asList(debitRule, creditRule)); + return reqVO; + } + + private List buildSpecialClosingSubjects() { + return new ArrayList<>(Arrays.asList( + new FmsSubjectDO().setId(301L).setCode("22210104"), + new FmsSubjectDO().setId(302L).setCode("222102"), + new FmsSubjectDO().setId(303L).setCode("540310"), + new FmsSubjectDO().setId(304L).setCode("222113"), + new FmsSubjectDO().setId(305L).setCode("540303"), + new FmsSubjectDO().setId(306L).setCode("222117"), + new FmsSubjectDO().setId(307L).setCode("540313"), + new FmsSubjectDO().setId(308L).setCode("222114"), + new FmsSubjectDO().setId(309L).setCode("5801"), + new FmsSubjectDO().setId(310L).setCode("222111"))); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateServiceImplTest.java new file mode 100644 index 0000000..3ef759e --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingTemplateServiceImplTest.java @@ -0,0 +1,259 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingTemplateMapper; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTemplateCategoryEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_SCHEME_RATIO_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_TEMPLATE_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_TEMPLATE_PRESET_SUBJECT_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsClosingTemplateServiceImpl.class) +public class FmsClosingTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsClosingTemplateServiceImpl closingTemplateService; + @Resource + private FmsClosingTemplateMapper closingTemplateMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsFinanceParameterService financeParameterService; + + @BeforeEach + public void before() { + when(financeParameterService.getFinanceParameter(any())).thenReturn(new FmsFinanceParameterDO() + .setSubjectCodeRule(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE)); + when(financeParameterService.convertStandardSubjectCode(any(), any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + public void testInitializeClosingTemplates() { + // mock 方法 + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(buildPresetSubjects()); + + // 调用 + closingTemplateService.initializeClosingTemplates(1L, 10L); + closingTemplateService.initializeClosingTemplates(1L, 10L); + + // 断言 + List templates = closingTemplateMapper.selectListByAccountSetId(1L); + assertEquals(21, templates.size()); + assertEquals(4L, templates.stream().map(FmsClosingTemplateDO::getCategory).distinct().count()); + assertEquals(21L, templates.stream().map(FmsClosingTemplateDO::getPresetCode).distinct().count()); + } + + @Test + public void testInitializeClosingTemplates_accountSetIsolation() { + // mock 方法 + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(buildPresetSubjects()); + when(subjectService.getSubjectList(2L, null, 10L)).thenReturn(buildPresetSubjects()); + + // 调用 + closingTemplateService.initializeClosingTemplates(1L, 10L); + closingTemplateService.initializeClosingTemplates(2L, 10L); + + // 断言 + assertEquals(21, closingTemplateMapper.selectListByAccountSetId(1L).size()); + assertEquals(21, closingTemplateMapper.selectListByAccountSetId(2L).size()); + } + + @Test + public void testInitializeClosingTemplates_subjectNotExists() { + // mock 方法 + List subjects = buildPresetSubjects(); + subjects.remove(0); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(subjects); + + // 调用,并断言 + assertServiceException(() -> closingTemplateService.initializeClosingTemplates(1L, 10L), + CLOSING_TEMPLATE_PRESET_SUBJECT_NOT_EXISTS, "daily-travel-reimbursement", "1001"); + assertEquals(0, closingTemplateMapper.selectCount()); + } + + @Test + public void testGetClosingTemplateList() { + // mock 数据 + closingTemplateMapper.insert(new FmsClosingTemplateDO().setAccountSetId(1L) + .setName("账套一模板").setCategory(FmsClosingTemplateCategoryEnum.DAILY_EXPENSE.getCategory())); + closingTemplateMapper.insert(new FmsClosingTemplateDO().setAccountSetId(2L) + .setName("账套二模板").setCategory(FmsClosingTemplateCategoryEnum.DAILY_EXPENSE.getCategory())); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + + // 调用 + List templates = closingTemplateService.getClosingTemplateList(1L, 10L); + + // 断言 + assertEquals(1, templates.size()); + verify(accountSetService).validateAccountSetReadPermission(1L, 10L); + } + + @Test + public void testCreateClosingTemplate() { + // mock 数据 + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(buildClosingTemplateSubjects()); + + // 准备参数 + FmsClosingTemplateSaveReqVO reqVO = buildClosingTemplateSaveReqVO(); + + // 调用 + Long id = closingTemplateService.createClosingTemplate(reqVO, 10L); + + // 断言 + FmsClosingTemplateDO template = closingTemplateMapper.selectById(id); + assertEquals("结转本月房租", template.getName()); + assertEquals(2, template.getSubjectRules().size()); + assertEquals("560102", template.getSubjectRules().get(0).getSubjectCode()); + assertEquals("1001", template.getSubjectRules().get(1).getSubjectCode()); + verify(accountSetService).validateAccountSetWritePermission(1L, 10L); + } + + @Test + public void testCreateClosingTemplate_ratioInvalid() { + // mock 数据 + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(buildClosingTemplateSubjects()); + + // 准备参数 + FmsClosingTemplateSaveReqVO reqVO = buildClosingTemplateSaveReqVO(); + reqVO.getSubjects().get(0).setAmountRatio(new BigDecimal("99")); + + // 调用,并断言异常 + assertServiceException(() -> closingTemplateService.createClosingTemplate(reqVO, 10L), + CLOSING_SCHEME_RATIO_INVALID); + assertEquals(0, closingTemplateMapper.selectCount()); + } + + @Test + public void testUpdateClosingTemplate() { + // mock 数据 + FmsClosingTemplateDO template = new FmsClosingTemplateDO().setAccountSetId(1L) + .setName("原模板").setCategory(FmsClosingTemplateCategoryEnum.DAILY_EXPENSE.getCategory()); + closingTemplateMapper.insert(template); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(buildClosingTemplateSubjects()); + + // 准备参数 + FmsClosingTemplateSaveReqVO reqVO = buildClosingTemplateSaveReqVO(); + reqVO.setId(template.getId()); + reqVO.setName("修改后模板"); + + // 调用 + closingTemplateService.updateClosingTemplate(reqVO, 10L); + + // 断言 + assertEquals("修改后模板", closingTemplateMapper.selectById(template.getId()).getName()); + } + + @Test + public void testUpdateClosingTemplate_notExists() { + // mock 数据 + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + + // 准备参数 + FmsClosingTemplateSaveReqVO reqVO = buildClosingTemplateSaveReqVO(); + reqVO.setId(999L); + + // 调用,并断言异常 + assertServiceException(() -> closingTemplateService.updateClosingTemplate(reqVO, 10L), + CLOSING_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testDeleteClosingTemplate() { + // mock 数据 + FmsClosingTemplateDO template = new FmsClosingTemplateDO().setAccountSetId(1L) + .setName("待删除模板").setCategory(FmsClosingTemplateCategoryEnum.DAILY_EXPENSE.getCategory()); + closingTemplateMapper.insert(template); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + + // 调用 + closingTemplateService.deleteClosingTemplate(1L, template.getId(), 10L); + + // 断言 + assertNull(closingTemplateMapper.selectById(template.getId())); + verify(accountSetService).validateAccountSetWritePermission(1L, 10L); + } + + // ========== 随机对象 ========== + + private List buildPresetSubjects() { + List subjectCodes = Arrays.asList("1001", "1002", "1122", "1221", "1405", "2202", "2211", + "22210101", "22210107", "222102", "222111", "222117", "5001", "560101", "560102", + "560107", "560110", "560209", "560302", "560303"); + List subjects = new ArrayList<>(); + for (int index = 0; index < subjectCodes.size(); index++) { + subjects.add(new FmsSubjectDO().setId((long) index + 1).setCode(subjectCodes.get(index)) + .setName("科目" + index)); + } + return subjects; + } + + private List buildClosingTemplateSubjects() { + return Arrays.asList( + new FmsSubjectDO().setId(101L).setCode("560102").setName("房租"), + new FmsSubjectDO().setId(102L).setCode("1001").setName("库存现金")); + } + + private FmsClosingTemplateSaveReqVO buildClosingTemplateSaveReqVO() { + FmsClosingTemplateSaveReqVO reqVO = new FmsClosingTemplateSaveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setName("结转本月房租"); + reqVO.setCategory(FmsClosingTemplateCategoryEnum.DAILY_EXPENSE.getCategory()); + reqVO.setPeriodEnd(true); + reqVO.setFormulaRule(FmsFormulaRuleEnum.BALANCE.getRule()); + reqVO.setTimeType(FmsClosingTimeTypeEnum.PERIOD_END.getType()); + reqVO.setSort(10); + FmsClosingTemplateSaveReqVO.SubjectRule debitRule = new FmsClosingTemplateSaveReqVO.SubjectRule(); + debitRule.setSubjectId(101L); + debitRule.setDigest("结转本月房租"); + debitRule.setDirection(FmsDebitCreditDirectionEnum.DEBIT.getType()); + debitRule.setAmountRatio(new BigDecimal("100")); + FmsClosingTemplateSaveReqVO.SubjectRule creditRule = new FmsClosingTemplateSaveReqVO.SubjectRule(); + creditRule.setSubjectId(102L); + creditRule.setDigest("结转本月房租"); + creditRule.setDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + creditRule.setAmountRatio(new BigDecimal("100")); + reqVO.setSubjects(Arrays.asList(debitRule, creditRule)); + return reqVO; + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherServiceImplTest.java new file mode 100644 index 0000000..cd4053d --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/closing/FmsClosingVoucherServiceImplTest.java @@ -0,0 +1,596 @@ +package cn.iocoder.yudao.module.fms.service.closing; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingVoucherGenerateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerEntryVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherEntrySaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.mysql.closing.FmsClosingVoucherMapper; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingVoucherTypeEnum; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_NOT_CURRENT_PERIOD; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CLOSING_SCHEME_NOT_PERIOD_END; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsClosingVoucherServiceImpl.class) +public class FmsClosingVoucherServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsClosingVoucherServiceImpl closingVoucherService; + @Resource + private FmsClosingVoucherMapper closingVoucherMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsClosingPeriodService closingPeriodService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsLedgerService ledgerService; + @MockitoBean + private FmsVoucherService voucherService; + @MockitoBean + private FmsVoucherWordService voucherWordService; + @MockitoBean + private FmsIncomeStatementService incomeStatementService; + + @Test + public void testGetClosingVoucherIdSet() { + // mock 数据 + closingVoucherMapper.insert(new FmsClosingVoucherDO() + .setAccountSetId(1L).setClosingId(11L).setVoucherId(21L)); + closingVoucherMapper.insert(new FmsClosingVoucherDO() + .setAccountSetId(2L).setClosingId(12L).setVoucherId(22L)); + + // 调用,并断言 + assertEquals(Collections.singleton(21L), closingVoucherService.getClosingVoucherIdSet( + 1L, Arrays.asList(21L, 22L, 23L))); + } + + @Test + public void testGetClosingVoucherListByClosingIdAndPeriod() { + // mock 数据 + closingVoucherMapper.insert(new FmsClosingVoucherDO().setAccountSetId(1L) + .setClosingId(11L).setVoucherId(21L) + .setVoucherTime(LocalDateTime.of(2026, 7, 31, 0, 0))); + closingVoucherMapper.insert(new FmsClosingVoucherDO().setAccountSetId(1L) + .setClosingId(11L).setVoucherId(22L) + .setVoucherTime(LocalDateTime.of(2026, 8, 31, 0, 0))); + closingVoucherMapper.insert(new FmsClosingVoucherDO().setAccountSetId(1L) + .setClosingId(12L).setVoucherId(23L) + .setVoucherTime(LocalDateTime.of(2026, 8, 31, 0, 0))); + + // 调用 + List result = closingVoucherService.getClosingVoucherListByClosingIdAndPeriod( + 11L, LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 9, 1, 0, 0)); + + // 断言 + assertEquals(1, result.size()); + assertEquals(22L, result.get(0).getVoucherId()); + } + + @Test + public void testGenerateProfitLossVoucher() { + // mock 数据 + FmsSubjectDO revenueSubject = new FmsSubjectDO().setId(101L).setCode("5001") + .setName("主营业务收入").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO expenseSubject = new FmsSubjectDO().setId(102L).setCode("5601") + .setName("销售费用").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO profitSubject = new FmsSubjectDO().setId(103L).setCode("3103") + .setName("本年利润").setType(FmsSubjectTypeEnum.EQUITY.getType()); + FmsSubjectDO adjustmentSubject = new FmsSubjectDO().setId(104L).setCode("6000") + .setName("以前年度损益调整").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO adjustmentClosingSubject = new FmsSubjectDO().setId(105L).setCode("3104") + .setName("利润分配").setType(FmsSubjectTypeEnum.EQUITY.getType()); + FmsLedgerSubjectBalanceRespVO revenueBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setPeriodDebitAmount(BigDecimal.ZERO).setPeriodCreditAmount(new BigDecimal("11300.00")) + .setChildren(Collections.emptyList()); + FmsLedgerSubjectBalanceRespVO expenseBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(102L) + .setPeriodDebitAmount(new BigDecimal("2200.00")).setPeriodCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()); + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Arrays.asList(revenueSubject, expenseSubject, profitSubject, + adjustmentSubject, adjustmentClosingSubject)); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Arrays.asList(revenueBalance, expenseBalance)); + when(voucherService.getNextVoucherNumber(eq(1L), eq(11L), any(), eq(10L))).thenReturn(4); + when(voucherService.createVoucher(any(), eq(10L))).thenReturn(99L); + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setId(1L) + .setName("结转损益").setPeriodEnd(true) + .setVoucherWordId(11L).setDigest("期末结转损益") + .setVoucherType(FmsClosingVoucherTypeEnum.COMBINED_GAIN_AND_LOSS.getType()) + .setPriorYearAdjustmentSubjectId(104L).setAdjustmentClosingSubjectId(105L) + .setOtherClosingSubjectId(103L).setReverseBalance(true) + .setType(FmsClosingTypeEnum.PROFIT_LOSS.getType()) + .setAccountSetId(1L).setClosingDay(31); + when(closingSchemeService.getClosingSchemeByAccountSetIdAndType( + 1L, FmsClosingTypeEnum.PROFIT_LOSS.getType())).thenReturn(closing); + + // 准备参数 + FmsProfitLossGenerateReqVO reqVO = new FmsProfitLossGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + + // 调用 + Long voucherId = closingVoucherService.generateProfitLossVoucher(reqVO, 10L); + + // 断言 + assertEquals(99L, voucherId); + ArgumentCaptor voucherCaptor = ArgumentCaptor.forClass(FmsVoucherSaveReqVO.class); + verify(voucherService).createVoucher(voucherCaptor.capture(), eq(10L)); + List entries = voucherCaptor.getValue().getEntries(); + assertEquals(3, entries.size()); + assertEquals(4, voucherCaptor.getValue().getVoucherNumber()); + assertEquals(new BigDecimal("11300.00"), entries.get(0).getDebitAmount()); + assertEquals(new BigDecimal("2200.00"), entries.get(1).getCreditAmount()); + assertEquals(new BigDecimal("9100.00"), entries.get(2).getCreditAmount()); + assertEquals("期末结转损益", entries.get(0).getDigest()); + List closingVouchers = closingVoucherMapper.selectListByPeriod(1L, + java.time.LocalDateTime.of(2026, 8, 1, 0, 0), + java.time.LocalDateTime.of(2026, 8, 31, 23, 59, 59)); + assertEquals(1, closingVouchers.size()); + assertEquals(99L, closingVouchers.get(0).getVoucherId()); + } + + @Test + public void testGenerateProfitLossVoucher_withAuxiliary() { + // mock 数据 + FmsSubjectDO expenseSubject = new FmsSubjectDO().setId(102L).setCode("5601") + .setName("销售费用").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()) + .setAuxiliaryTypeIds(Collections.singletonList(11L)); + FmsSubjectDO profitSubject = new FmsSubjectDO().setId(103L).setCode("3103") + .setName("本年利润").setType(FmsSubjectTypeEnum.EQUITY.getType()); + FmsSubjectDO adjustmentSubject = new FmsSubjectDO().setId(104L).setCode("6000") + .setName("以前年度损益调整").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO adjustmentClosingSubject = new FmsSubjectDO().setId(105L).setCode("3104") + .setName("利润分配").setType(FmsSubjectTypeEnum.EQUITY.getType()); + FmsLedgerSubjectBalanceRespVO expenseBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(102L) + .setPeriodDebitAmount(new BigDecimal("200.00")).setPeriodCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()); + FmsLedgerEntryVO ledgerEntry = new FmsLedgerEntryVO().setSubjectId(102L) + .setDebitAmount(new BigDecimal("200.00")).setCreditAmount(BigDecimal.ZERO) + .setAuxiliaries(Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .type(1).typeId(11L).itemId(21L).name("回归客户").build())); + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn( + new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + expenseSubject, profitSubject, adjustmentSubject, adjustmentClosingSubject)); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(expenseBalance)); + when(ledgerService.getEntryList(1L, LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTime.of(2026, 9, 1, 0, 0), 10L)).thenReturn(Collections.singletonList(ledgerEntry)); + when(voucherService.getNextVoucherNumber(eq(1L), eq(11L), any(), eq(10L))).thenReturn(4); + when(voucherService.createVoucher(any(), eq(10L))).thenReturn(99L); + when(closingSchemeService.getClosingSchemeByAccountSetIdAndType( + 1L, FmsClosingTypeEnum.PROFIT_LOSS.getType())).thenReturn(new FmsClosingSchemeDO().setId(1L) + .setName("结转损益").setPeriodEnd(true).setVoucherWordId(11L).setDigest("期末结转损益") + .setVoucherType(FmsClosingVoucherTypeEnum.COMBINED_GAIN_AND_LOSS.getType()) + .setPriorYearAdjustmentSubjectId(104L).setAdjustmentClosingSubjectId(105L) + .setOtherClosingSubjectId(103L).setReverseBalance(true) + .setType(FmsClosingTypeEnum.PROFIT_LOSS.getType()).setAccountSetId(1L).setClosingDay(31)); + + // 准备参数 + FmsProfitLossGenerateReqVO reqVO = new FmsProfitLossGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + + // 调用 + closingVoucherService.generateProfitLossVoucher(reqVO, 10L); + + // 断言 + ArgumentCaptor voucherCaptor = ArgumentCaptor.forClass(FmsVoucherSaveReqVO.class); + verify(voucherService).createVoucher(voucherCaptor.capture(), eq(10L)); + List entries = voucherCaptor.getValue().getEntries(); + assertEquals(2, entries.size()); + assertEquals(4, voucherCaptor.getValue().getVoucherNumber()); + assertEquals(102L, entries.get(0).getSubjectId()); + assertEquals(new BigDecimal("200.00"), entries.get(0).getCreditAmount()); + assertEquals(1, entries.get(0).getAuxiliaries().size()); + assertEquals(11L, entries.get(0).getAuxiliaries().get(0).getTypeId()); + assertEquals(21L, entries.get(0).getAuxiliaries().get(0).getItemId()); + assertEquals(103L, entries.get(1).getSubjectId()); + assertEquals(new BigDecimal("200.00"), entries.get(1).getDebitAmount()); + } + + @Test + public void testGenerateProfitLossVoucher_separateGainAndLoss() { + // mock 数据 + FmsSubjectDO revenueSubject = new FmsSubjectDO().setId(101L).setCode("5001") + .setName("主营业务收入").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO expenseSubject = new FmsSubjectDO().setId(102L).setCode("5601") + .setName("销售费用").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO profitSubject = new FmsSubjectDO().setId(103L).setCode("3103") + .setName("本年利润").setType(FmsSubjectTypeEnum.EQUITY.getType()); + FmsSubjectDO adjustmentSubject = new FmsSubjectDO().setId(104L).setCode("6000") + .setName("以前年度损益调整").setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO adjustmentClosingSubject = new FmsSubjectDO().setId(105L).setCode("3104") + .setName("利润分配").setType(FmsSubjectTypeEnum.EQUITY.getType()); + FmsLedgerSubjectBalanceRespVO revenueBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setPeriodDebitAmount(BigDecimal.ZERO).setPeriodCreditAmount(new BigDecimal("11300.00")) + .setChildren(Collections.emptyList()); + FmsLedgerSubjectBalanceRespVO expenseBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(102L) + .setPeriodDebitAmount(new BigDecimal("2200.00")).setPeriodCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()); + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + revenueSubject, expenseSubject, profitSubject, + adjustmentSubject, adjustmentClosingSubject)); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Arrays.asList(revenueBalance, expenseBalance)); + when(voucherService.createVoucher(any(), eq(10L))).thenReturn(99L, 100L); + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setId(1L) + .setName("结转损益").setPeriodEnd(true) + .setVoucherWordId(11L).setDigest("结转损益") + .setVoucherType(FmsClosingVoucherTypeEnum.SEPARATE_GAIN_AND_LOSS.getType()) + .setPriorYearAdjustmentSubjectId(104L).setAdjustmentClosingSubjectId(105L) + .setOtherClosingSubjectId(103L).setReverseBalance(true) + .setType(FmsClosingTypeEnum.PROFIT_LOSS.getType()) + .setAccountSetId(1L).setClosingDay(30); + when(closingSchemeService.getClosingSchemeByAccountSetIdAndType( + 1L, FmsClosingTypeEnum.PROFIT_LOSS.getType())).thenReturn(closing); + + // 准备参数 + FmsProfitLossGenerateReqVO reqVO = new FmsProfitLossGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + + // 调用 + Long voucherId = closingVoucherService.generateProfitLossVoucher(reqVO, 10L); + + // 断言 + assertEquals(99L, voucherId); + ArgumentCaptor voucherCaptor = ArgumentCaptor.forClass(FmsVoucherSaveReqVO.class); + verify(voucherService, times(2)).createVoucher(voucherCaptor.capture(), eq(10L)); + List vouchers = voucherCaptor.getAllValues(); + assertEquals(2, vouchers.size()); + assertEquals(2, vouchers.get(0).getEntries().size()); + assertEquals(new BigDecimal("11300.00"), vouchers.get(0).getEntries().get(0).getDebitAmount()); + assertEquals(new BigDecimal("11300.00"), vouchers.get(0).getEntries().get(1).getCreditAmount()); + assertEquals(2, vouchers.get(1).getEntries().size()); + assertEquals(new BigDecimal("2200.00"), vouchers.get(1).getEntries().get(0).getCreditAmount()); + assertEquals(new BigDecimal("2200.00"), vouchers.get(1).getEntries().get(1).getDebitAmount()); + assertEquals(LocalDateTime.of(2026, 8, 30, 0, 0), vouchers.get(0).getVoucherTime()); + assertEquals(2, closingVoucherMapper.selectListByPeriod(1L, + LocalDateTime.of(2026, 8, 1, 0, 0), + LocalDateTime.of(2026, 8, 31, 23, 59, 59)).size()); + } + + @Test + public void testGenerateClosingSchemeVoucher_yearBeginBalance() { + // mock 数据 + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setId(1L) + .setName("结转本月房租").setPeriodEnd(true) + .setSubjectId(101L).setFormulaRule(FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + .setTimeType(FmsClosingTimeTypeEnum.YEAR_BEGIN.getType()).setVoucherWordId(11L) + .setType(FmsClosingTypeEnum.REGULAR.getType()).setAccountSetId(1L) + .setSubjectRules(Arrays.asList( + FmsClosingSchemeDO.SubjectRule.builder().subjectId(102L).digest("结转本月房租") + .direction(FmsDebitCreditDirectionEnum.DEBIT.getType()) + .amountRatio(new BigDecimal("100")).build(), + FmsClosingSchemeDO.SubjectRule.builder().subjectId(103L).digest("结转本月房租") + .direction(FmsDebitCreditDirectionEnum.CREDIT.getType()) + .amountRatio(new BigDecimal("100")).build())); + when(closingSchemeService.validateClosingSchemeExists(1L, closing.getId())).thenReturn(closing); + closingVoucherMapper.insert(new FmsClosingVoucherDO().setClosingId(closing.getId()) + .setVoucherId(77L).setVoucherTime(LocalDateTime.of(2026, 8, 31, 0, 0)) + .setAmount(new BigDecimal("1200.00")).setAccountSetId(1L)); + FmsLedgerSubjectBalanceRespVO sourceBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setOpeningDebitAmount(new BigDecimal("100.00")) + .setOpeningCreditAmount(BigDecimal.ZERO) + .setEndingDebitAmount(new BigDecimal("1200.00")) + .setEndingCreditAmount(BigDecimal.ZERO).setChildren(Collections.emptyList()); + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(sourceBalance)); + when(voucherService.getNextVoucherNumber(eq(1L), eq(11L), any(), eq(10L))).thenReturn(4); + when(voucherService.createVoucher(any(), eq(10L))).thenReturn(88L); + doAnswer(invocation -> { + assertEquals(0, closingVoucherMapper.selectListByClosingId(closing.getId()).size()); + return null; + }).when(voucherService).deleteVoucherList(1L, Collections.singletonList(77L), 10L); + + // 准备参数 + FmsClosingSchemeGenerateReqVO reqVO = new FmsClosingSchemeGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setId(closing.getId()); + reqVO.setMonth("2026-08"); + + // 调用 + Long voucherId = closingVoucherService.generateClosingSchemeVoucher(reqVO, 10L); + + // 断言 + assertEquals(88L, voucherId); + ArgumentCaptor voucherCaptor = ArgumentCaptor.forClass(FmsVoucherSaveReqVO.class); + verify(voucherService).createVoucher(voucherCaptor.capture(), eq(10L)); + verify(voucherService).deleteVoucherList(1L, Collections.singletonList(77L), 10L); + assertEquals(4, voucherCaptor.getValue().getVoucherNumber()); + assertEquals(new BigDecimal("100.00"), + voucherCaptor.getValue().getEntries().get(0).getDebitAmount()); + assertEquals(new BigDecimal("100.00"), + voucherCaptor.getValue().getEntries().get(1).getCreditAmount()); + List closingVouchers = closingVoucherMapper.selectListByClosingId(closing.getId()); + assertEquals(1, closingVouchers.size()); + assertEquals(88L, closingVouchers.get(0).getVoucherId()); + assertEquals(0, new BigDecimal("100.00").compareTo(closingVouchers.get(0).getAmount())); + } + + @Test + public void testGenerateClosingSchemeVoucher_nonPeriodEnd() { + // mock 数据 + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn( + new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setId(1L) + .setAccountSetId(1L).setType(FmsClosingTypeEnum.REGULAR.getType()).setPeriodEnd(false); + when(closingSchemeService.validateClosingSchemeExists(1L, closing.getId())).thenReturn(closing); + // 准备参数 + FmsClosingSchemeGenerateReqVO reqVO = new FmsClosingSchemeGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setId(closing.getId()); + reqVO.setMonth("2026-08"); + + // 调用,并断言 + assertServiceException(() -> closingVoucherService.generateClosingSchemeVoucher(reqVO, 10L), + CLOSING_SCHEME_NOT_PERIOD_END); + } + + @Test + public void testGenerateClosingSchemeVoucher_localTax() { + // mock 数据 + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setId(1L) + .setName("计提地税").setPeriodEnd(true) + .setVoucherWordId(11L).setType(FmsClosingTypeEnum.LOCAL_TAX.getType()) + .setAccountSetId(1L).setSubjectRules(Arrays.asList( + buildSubjectRule(301L, "计提教育费附加", 1, "3"), + buildSubjectRule(302L, "计提教育费附加", 2, "3"), + buildSubjectRule(303L, "计提城建费", 1, "7"), + buildSubjectRule(304L, "计提城建费", 2, "7"), + buildSubjectRule(305L, "计提地方教育费附加", 1, "2"), + buildSubjectRule(306L, "计提地方教育费附加", 2, "2"))); + when(closingSchemeService.validateClosingSchemeExists(1L, closing.getId())).thenReturn(closing); + List subjects = Arrays.asList( + new FmsSubjectDO().setId(201L).setCode("222101").setName("应交增值税"), + new FmsSubjectDO().setId(202L).setCode("222121").setName("应交消费税")); + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(subjects); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(Arrays.asList( + new FmsLedgerSubjectBalanceRespVO().setSubjectId(201L) + .setEndingDebitAmount(BigDecimal.ZERO).setEndingCreditAmount(new BigDecimal("1000.00")) + .setChildren(Collections.emptyList()), + new FmsLedgerSubjectBalanceRespVO().setSubjectId(202L) + .setEndingDebitAmount(BigDecimal.ZERO).setEndingCreditAmount(new BigDecimal("500.00")) + .setChildren(Collections.emptyList()))); + when(voucherService.createVoucher(any(), eq(10L))).thenReturn(88L); + + // 准备参数 + FmsClosingSchemeGenerateReqVO reqVO = new FmsClosingSchemeGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setId(closing.getId()); + reqVO.setMonth("2026-08"); + + // 调用 + Long voucherId = closingVoucherService.generateClosingSchemeVoucher(reqVO, 10L); + + // 断言 + assertEquals(88L, voucherId); + ArgumentCaptor voucherCaptor = ArgumentCaptor.forClass(FmsVoucherSaveReqVO.class); + verify(voucherService).createVoucher(voucherCaptor.capture(), eq(10L)); + List entries = voucherCaptor.getValue().getEntries(); + assertEquals(new BigDecimal("45.00"), entries.get(0).getDebitAmount()); + assertEquals(new BigDecimal("105.00"), entries.get(2).getDebitAmount()); + assertEquals(new BigDecimal("30.00"), entries.get(4).getDebitAmount()); + assertEquals(0, new BigDecimal("180.00").compareTo( + closingVoucherMapper.selectListByClosingId(closing.getId()).get(0).getAmount())); + } + + @Test + public void testGenerateClosingVoucherList_skipSchemeWithoutBalance() { + // mock 数据 + List subjectRules = Arrays.asList( + FmsClosingSchemeDO.SubjectRule.builder().subjectId(102L).digest("期末结转") + .direction(FmsDebitCreditDirectionEnum.DEBIT.getType()) + .amountRatio(new BigDecimal("100")).build(), + FmsClosingSchemeDO.SubjectRule.builder().subjectId(103L).digest("期末结转") + .direction(FmsDebitCreditDirectionEnum.CREDIT.getType()) + .amountRatio(new BigDecimal("100")).build()); + FmsClosingSchemeDO firstClosing = new FmsClosingSchemeDO().setId(1L) + .setName("结转本月房租").setPeriodEnd(true) + .setSubjectId(101L).setFormulaRule(FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + .setTimeType(FmsClosingTimeTypeEnum.PERIOD_END.getType()).setVoucherWordId(11L) + .setType(FmsClosingTypeEnum.REGULAR.getType()).setAccountSetId(1L) + .setSubjectRules(subjectRules); + FmsClosingSchemeDO secondClosing = new FmsClosingSchemeDO().setId(2L) + .setName("结转本月物业费").setPeriodEnd(true) + .setSubjectId(101L).setFormulaRule(FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + .setTimeType(FmsClosingTimeTypeEnum.PERIOD_END.getType()).setVoucherWordId(11L) + .setType(FmsClosingTypeEnum.REGULAR.getType()).setAccountSetId(1L) + .setSubjectRules(subjectRules); + when(closingSchemeService.validateClosingSchemeExists(1L, firstClosing.getId())).thenReturn(firstClosing); + when(closingSchemeService.validateClosingSchemeExists(1L, secondClosing.getId())).thenReturn(secondClosing); + FmsLedgerSubjectBalanceRespVO sourceBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setEndingDebitAmount(new BigDecimal("1200.00")) + .setEndingCreditAmount(BigDecimal.ZERO).setChildren(Collections.emptyList()); + FmsLedgerSubjectBalanceRespVO zeroBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setEndingDebitAmount(BigDecimal.ZERO) + .setEndingCreditAmount(BigDecimal.ZERO).setChildren(Collections.emptyList()); + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn( + new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(sourceBalance), Collections.singletonList(zeroBalance)); + when(voucherService.createVoucher(any(), eq(10L))).thenReturn(88L); + LocalDateTime voucherTime = LocalDateTime.of(2026, 8, 31, 0, 0); + closingVoucherMapper.insert(new FmsClosingVoucherDO().setClosingId(firstClosing.getId()) + .setVoucherId(801L).setVoucherTime(voucherTime).setAmount(new BigDecimal("1200.00")) + .setClosed(false).setAccountSetId(1L)); + closingVoucherMapper.insert(new FmsClosingVoucherDO().setClosingId(secondClosing.getId()) + .setVoucherId(802L).setVoucherTime(voucherTime).setAmount(new BigDecimal("1200.00")) + .setClosed(false).setAccountSetId(1L)); + + // 准备参数 + FmsClosingVoucherGenerateReqVO reqVO = new FmsClosingVoucherGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setIds(Arrays.asList(firstClosing.getId(), secondClosing.getId())); + reqVO.setMonth("2026-08"); + + // 调用 + List voucherIds = closingVoucherService.generateClosingVoucherList(reqVO, 10L); + + // 断言 + assertEquals(Collections.singletonList(88L), voucherIds); + assertEquals(1, closingVoucherMapper.selectListByClosingId(firstClosing.getId()).size()); + assertEquals(0, closingVoucherMapper.selectListByClosingId(secondClosing.getId()).size()); + verify(voucherService).deleteVoucherList(1L, Arrays.asList(801L, 802L), 10L); + verify(voucherService, times(1)).createVoucher(any(), eq(10L)); + } + + @Test + public void testGenerateClosingVoucherList_nonPeriodEnd() { + // mock 数据 + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn( + new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + FmsClosingSchemeDO closing = new FmsClosingSchemeDO().setId(1L) + .setAccountSetId(1L).setType(FmsClosingTypeEnum.REGULAR.getType()).setPeriodEnd(false); + when(closingSchemeService.validateClosingSchemeExists(1L, closing.getId())).thenReturn(closing); + // 准备参数 + FmsClosingVoucherGenerateReqVO reqVO = new FmsClosingVoucherGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setIds(Collections.singletonList(closing.getId())); + reqVO.setMonth("2026-08"); + + // 调用,并断言 + assertServiceException(() -> closingVoucherService.generateClosingVoucherList(reqVO, 10L), + CLOSING_SCHEME_NOT_PERIOD_END); + } + + @Test + public void testGenerateClosingVoucherList_rollbackWhenLaterSchemeFails() { + // mock 数据 + List subjectRules = Arrays.asList( + FmsClosingSchemeDO.SubjectRule.builder().subjectId(102L).digest("期末结转") + .direction(FmsDebitCreditDirectionEnum.DEBIT.getType()) + .amountRatio(new BigDecimal("100")).build(), + FmsClosingSchemeDO.SubjectRule.builder().subjectId(103L).digest("期末结转") + .direction(FmsDebitCreditDirectionEnum.CREDIT.getType()) + .amountRatio(new BigDecimal("100")).build()); + FmsClosingSchemeDO firstClosing = new FmsClosingSchemeDO().setId(1L) + .setName("结转本月房租").setPeriodEnd(true) + .setSubjectId(101L).setFormulaRule(FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + .setTimeType(FmsClosingTimeTypeEnum.PERIOD_END.getType()).setVoucherWordId(11L) + .setType(FmsClosingTypeEnum.REGULAR.getType()).setAccountSetId(1L) + .setSubjectRules(subjectRules); + FmsClosingSchemeDO secondClosing = new FmsClosingSchemeDO().setId(2L) + .setName("结转本月物业费").setPeriodEnd(true) + .setSubjectId(101L).setFormulaRule(FmsFormulaRuleEnum.DEBIT_BALANCE.getRule()) + .setTimeType(FmsClosingTimeTypeEnum.PERIOD_END.getType()).setVoucherWordId(11L) + .setType(FmsClosingTypeEnum.REGULAR.getType()).setAccountSetId(1L) + .setSubjectRules(subjectRules); + when(closingSchemeService.validateClosingSchemeExists(1L, firstClosing.getId())).thenReturn(firstClosing); + when(closingSchemeService.validateClosingSchemeExists(1L, secondClosing.getId())).thenReturn(secondClosing); + FmsLedgerSubjectBalanceRespVO sourceBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setEndingDebitAmount(new BigDecimal("1200.00")) + .setEndingCreditAmount(BigDecimal.ZERO).setChildren(Collections.emptyList()); + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(sourceBalance)); + when(voucherService.createVoucher(any(), eq(10L))).thenReturn(88L) + .thenThrow(new IllegalStateException("第二个方案生成失败")); + + // 准备参数 + FmsClosingVoucherGenerateReqVO reqVO = new FmsClosingVoucherGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setIds(Arrays.asList(firstClosing.getId(), secondClosing.getId())); + reqVO.setMonth("2026-08"); + + // 调用,并断言 + assertThrows(IllegalStateException.class, + () -> closingVoucherService.generateClosingVoucherList(reqVO, 10L)); + assertEquals(0, closingVoucherMapper.selectListByClosingId(firstClosing.getId()).size()); + assertEquals(0, closingVoucherMapper.selectListByClosingId(secondClosing.getId()).size()); + } + + @Test + public void testGenerateProfitLossVoucher_notCurrentPeriod() { + // mock 数据 + LocalDateTime startTime = LocalDateTime.of(2026, 7, 1, 0, 0); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(true).setStartTime(startTime)); + when(closingPeriodService.getCurrentMonth(1L, startTime)).thenReturn(YearMonth.of(2026, 8)); + + // 准备参数 + FmsProfitLossGenerateReqVO reqVO = new FmsProfitLossGenerateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-09"); + + // 调用,并断言 + assertServiceException(() -> closingVoucherService.generateProfitLossVoucher(reqVO, 10L), + CLOSING_NOT_CURRENT_PERIOD); + } + + private FmsClosingSchemeDO.SubjectRule buildSubjectRule( + Long subjectId, String digest, Integer direction, String amountRatio) { + return FmsClosingSchemeDO.SubjectRule.builder().subjectId(subjectId).digest(digest) + .direction(direction).amountRatio(new BigDecimal(amountRatio)).build(); + } + + // ========== 随机对象 ========== + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetServiceImplTest.java new file mode 100644 index 0000000..f291cb7 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountSetServiceImplTest.java @@ -0,0 +1,292 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAccountSetMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountUserLevelEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountingStandardEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsCurrencyPresetEnum; +import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingTemplateService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import cn.iocoder.yudao.module.fms.service.config.FmsCurrencyService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_ACCESS_DENIED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_ALREADY_INITIALIZED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@Import(FmsAccountSetServiceImpl.class) +public class FmsAccountSetServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsAccountSetServiceImpl accountSetService; + @Resource + private FmsAccountSetMapper accountSetMapper; + + @MockitoBean + private FmsAccountUserService accountUserService; + @MockitoBean + private FmsCurrencyService currencyService; + @MockitoBean + private FmsFinanceParameterService financeParameterService; + @MockitoBean + private FmsVoucherWordService voucherWordService; + @MockitoBean + private FmsAuxiliaryTypeService auxiliaryTypeService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + @MockitoBean + private FmsClosingTemplateService closingTemplateService; + + @Test + public void testCreateAccountSet_success() { + // 准备参数 + Long userId = randomLongId(); + FmsAccountSetSaveReqVO reqVO = buildAccountSetSaveReqVO("TEST001", "测试科技有限公司"); + + // 调用 + Long accountSetId = accountSetService.createAccountSet(reqVO, userId); + + // 断言 + FmsAccountSetDO accountSet = accountSetMapper.selectById(accountSetId); + assertPojoEquals(reqVO, accountSet, "id"); + assertFalse(accountSet.getInitialized()); + verify(accountUserService).createAccountOwner(accountSetId, userId); + } + + @Test + public void testCreateAccountSet_codeDuplicate() { + // mock 数据 + accountSetMapper.insert(buildAccountSetDO("WK001", false)); + // 准备参数 + FmsAccountSetSaveReqVO reqVO = buildAccountSetSaveReqVO("WK001", "芋道科技有限公司"); + + // 调用,并断言异常 + assertServiceException(() -> accountSetService.createAccountSet(reqVO, randomLongId()), + ACCOUNT_SET_CODE_DUPLICATE); + } + + @Test + public void testGetAccountSet_success() { + // mock 数据 + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", false); + accountSetMapper.insert(accountSet); + + // 调用 + FmsAccountSetDO result = accountSetService.getAccountSet(accountSet.getId()); + + // 断言 + assertPojoEquals(accountSet, result, "deleted"); + verifyNoInteractions(accountUserService); + } + + @Test + public void testValidateAccountSetReadPermission_notMemberDenied() { + // mock 数据 + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", false); + accountSetMapper.insert(accountSet); + Long userId = randomLongId(); + // 调用,并断言异常 + assertServiceException(() -> accountSetService.validateAccountSetReadPermission(accountSet.getId(), userId), + ACCOUNT_SET_ACCESS_DENIED); + } + + @Test + public void testValidateAccountSetReadPermission_invalidLevelDenied() { + // mock 数据 + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", false); + accountSetMapper.insert(accountSet); + Long userId = randomLongId(); + when(accountUserService.getAccountUser(accountSet.getId(), userId)) + .thenReturn(new FmsAccountUserDO().setLevel(99)); + + // 调用,并断言异常 + assertServiceException(() -> accountSetService.validateAccountSetReadPermission(accountSet.getId(), userId), + ACCOUNT_SET_ACCESS_DENIED); + } + + @Test + public void testValidateAccountSetWritePermission_readDenied() { + // mock 数据 + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", false); + accountSetMapper.insert(accountSet); + Long userId = randomLongId(); + when(accountUserService.getAccountUser(accountSet.getId(), userId)).thenReturn(new FmsAccountUserDO() + .setLevel(FmsAccountUserLevelEnum.READ.getLevel())); + + // 调用,并断言异常 + assertServiceException(() -> accountSetService.validateAccountSetWritePermission(accountSet.getId(), userId), + ACCOUNT_SET_ACCESS_DENIED); + } + + @Test + public void testValidateAccountSetWritePermission_writeSuccess() { + // mock 数据 + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", false); + accountSetMapper.insert(accountSet); + Long userId = randomLongId(); + when(accountUserService.getAccountUser(accountSet.getId(), userId)).thenReturn(new FmsAccountUserDO() + .setLevel(FmsAccountUserLevelEnum.WRITE.getLevel())); + + // 调用 + FmsAccountSetDO result = accountSetService.validateAccountSetWritePermission(accountSet.getId(), userId); + + // 断言 + assertEquals(accountSet.getId(), result.getId()); + } + + @Test + public void testValidateAccountSetOwnerPermission_writeDenied() { + // mock 数据 + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", false); + accountSetMapper.insert(accountSet); + Long userId = randomLongId(); + when(accountUserService.getAccountUser(accountSet.getId(), userId)).thenReturn(new FmsAccountUserDO() + .setLevel(FmsAccountUserLevelEnum.WRITE.getLevel())); + + // 调用,并断言异常 + assertServiceException(() -> accountSetService.validateAccountSetOwnerPermission(accountSet.getId(), userId), + ACCOUNT_SET_ACCESS_DENIED); + } + + @Test + public void testGetAccountSetList() { + // mock 数据 + FmsAccountSetDO firstAccountSet = buildAccountSetDO("WK001", true); + accountSetMapper.insert(firstAccountSet); + FmsAccountSetDO secondAccountSet = buildAccountSetDO("WK002", false); + accountSetMapper.insert(secondAccountSet); + + // 调用 + List accountSets = accountSetService.getAccountSetList( + Arrays.asList(firstAccountSet.getId(), secondAccountSet.getId())); + + // 断言 + assertEquals(2, accountSets.size()); + } + + @Test + public void testLockAccountSet_success() { + // mock 数据 + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", true); + accountSetMapper.insert(accountSet); + + // 调用 + accountSetService.lockAccountSet(accountSet.getId()); + + // 断言 + assertEquals(accountSet.getId(), accountSetMapper.selectById(accountSet.getId()).getId()); + } + + @Test + public void testLockAccountSet_notExists() { + // 调用,并断言异常 + assertServiceException(() -> accountSetService.lockAccountSet(randomLongId()), + ACCOUNT_SET_NOT_EXISTS); + } + + @Test + public void testInitializeAccountSet_success() { + // mock 数据 + Long userId = randomLongId(); + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", false); + accountSetMapper.insert(accountSet); + when(accountUserService.getAccountUser(accountSet.getId(), userId)).thenReturn(new FmsAccountUserDO() + .setLevel(FmsAccountUserLevelEnum.WRITE.getLevel())); + when(currencyService.initializeStandardCurrency(accountSet.getId(), FmsCurrencyPresetEnum.RMB)) + .thenReturn(new FmsCurrencyDO().setId(100L)); + // 准备参数 + FmsAccountSetInitializeReqVO reqVO = buildInitializeReqVO(accountSet.getId()); + + // 调用 + accountSetService.initializeAccountSet(reqVO, userId); + + // 断言 + FmsAccountSetDO dbAccountSet = accountSetMapper.selectById(accountSet.getId()); + assertTrue(dbAccountSet.getInitialized()); + assertEquals(LocalDateTime.of(2026, 8, 1, 0, 0), dbAccountSet.getStartTime()); + assertEquals(reqVO.getStandard(), dbAccountSet.getStandard()); + assertEquals(100L, dbAccountSet.getCurrencyId()); + verify(accountUserService).getAccountUser(accountSet.getId(), userId); + verify(financeParameterService).initializeFinanceParameter(accountSet.getId(), reqVO); + verify(voucherWordService).initializeDefaultVoucherWords(accountSet.getId()); + verify(auxiliaryTypeService).initializeDefaultTypes(accountSet.getId()); + verify(subjectService).initializeDefaultSubjects(accountSet.getId()); + verify(closingTemplateService).initializeClosingTemplates(accountSet.getId(), userId); + verify(closingSchemeService).initializeDefaultClosingSchemes(accountSet.getId(), userId); + } + + @Test + public void testInitializeAccountSet_alreadyInitialized() { + // mock 数据 + Long userId = randomLongId(); + FmsAccountSetDO accountSet = buildAccountSetDO("WK001", true); + accountSetMapper.insert(accountSet); + when(accountUserService.getAccountUser(accountSet.getId(), userId)).thenReturn(new FmsAccountUserDO() + .setLevel(FmsAccountUserLevelEnum.OWNER.getLevel())); + // 准备参数 + FmsAccountSetInitializeReqVO reqVO = buildInitializeReqVO(accountSet.getId()); + + // 调用,并断言异常 + assertServiceException(() -> accountSetService.initializeAccountSet(reqVO, userId), + ACCOUNT_SET_ALREADY_INITIALIZED); + verifyNoInteractions(currencyService, financeParameterService, + voucherWordService, auxiliaryTypeService, subjectService, + closingTemplateService, closingSchemeService); + } + + // ========== 随机对象 ========== + + private FmsAccountSetInitializeReqVO buildInitializeReqVO(Long accountSetId) { + return new FmsAccountSetInitializeReqVO().setAccountSetId(accountSetId) + .setCurrencyCode(FmsCurrencyPresetEnum.RMB.getCode()) + .setStartTime(LocalDateTime.of(2026, 8, 23, 12, 30)) + .setStandard(FmsAccountingStandardEnum.SMALL_BUSINESS_2013.getStandard()) + .setLevel(FmsFinanceParameterDO.DEFAULT_LEVEL) + .setSubjectCodeRule(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE) + .setLedgerBalanceMode(FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode()); + } + + private FmsAccountSetSaveReqVO buildAccountSetSaveReqVO(String code, String companyName) { + return randomPojo(FmsAccountSetSaveReqVO.class, reqVO -> reqVO + .setCompanyCode(code).setCompanyName(companyName) + .setCompanyProfile("企业数字化服务").setIndustry("软件和信息技术服务业") + .setContactName("张三").setMobile("15601691399").setEmail("finance@example.com")); + } + + private FmsAccountSetDO buildAccountSetDO(String code, Boolean initialized) { + return randomPojo(FmsAccountSetDO.class, accountSet -> accountSet.setId(null) + .setCompanyCode(code).setCompanyName(code + " 有限公司").setInitialized(initialized)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserServiceImplTest.java new file mode 100644 index 0000000..6614436 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAccountUserServiceImplTest.java @@ -0,0 +1,149 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser.FmsAccountUserUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAccountUserMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAccountUserLevelEnum; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsAccountUserServiceImpl.class) +public class FmsAccountUserServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsAccountUserServiceImpl accountUserService; + @Resource + private FmsAccountUserMapper accountUserMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private AdminUserApi adminUserApi; + + @Test + public void testCreateAccountOwner_firstAccountSet() { + // 准备参数 + Long accountSetId = randomLongId(); + Long userId = randomLongId(); + + // 调用 + accountUserService.createAccountOwner(accountSetId, userId); + + // 断言 + FmsAccountUserDO accountUser = accountUserMapper.selectByAccountSetIdAndUserId(accountSetId, userId); + assertTrue(accountUser.getDefaultStatus()); + assertTrue(accountUser.getFounder()); + assertEquals(FmsAccountUserLevelEnum.OWNER.getLevel(), accountUser.getLevel()); + } + + @Test + public void testGetAccountUserList_founderFirst() { + // mock 数据 + Long accountSetId = randomLongId(); + Long userId = randomLongId(); + Long founderUserId = randomLongId(); + accountUserMapper.insert(new FmsAccountUserDO().setAccountSetId(accountSetId).setUserId(userId) + .setDefaultStatus(false).setFounder(false) + .setLevel(FmsAccountUserLevelEnum.READ.getLevel())); + accountUserMapper.insert(new FmsAccountUserDO().setAccountSetId(accountSetId).setUserId(founderUserId) + .setDefaultStatus(true).setFounder(true) + .setLevel(FmsAccountUserLevelEnum.OWNER.getLevel())); + + // 调用 + List accountUsers = accountUserService.getAccountUserList(accountSetId, userId); + + // 断言 + assertEquals(2, accountUsers.size()); + assertEquals(founderUserId, accountUsers.get(0).getUserId()); + assertTrue(accountUsers.get(0).getFounder()); + verify(accountSetService).validateAccountSetReadPermission(accountSetId, userId); + } + + @Test + public void testGetAccountUser_notExists() { + // 调用,并断言 + assertNull(accountUserService.getAccountUser(randomLongId(), randomLongId())); + } + + @Test + public void testUpdateAccountUserList_replaceAndRejoin() { + // mock 数据 + Long accountSetId = randomLongId(); + Long founderUserId = randomLongId(); + Long oldMemberUserId = randomLongId(); + Long newMemberUserId = randomLongId(); + when(accountSetService.validateAccountSetOwnerPermission(accountSetId, founderUserId)) + .thenReturn(new FmsAccountSetDO().setId(accountSetId)); + accountUserMapper.insert(new FmsAccountUserDO().setAccountSetId(accountSetId) + .setUserId(founderUserId).setDefaultStatus(true).setFounder(true) + .setLevel(FmsAccountUserLevelEnum.OWNER.getLevel())); + accountUserMapper.insert(new FmsAccountUserDO().setAccountSetId(accountSetId) + .setUserId(oldMemberUserId).setDefaultStatus(false).setFounder(false) + .setLevel(FmsAccountUserLevelEnum.READ.getLevel())); + // 准备参数 + FmsAccountUserUpdateReqVO reqVO = new FmsAccountUserUpdateReqVO().setAccountSetId(accountSetId) + .setMembers(Arrays.asList( + buildAccountUserMember(founderUserId, FmsAccountUserLevelEnum.OWNER), + buildAccountUserMember(oldMemberUserId, FmsAccountUserLevelEnum.WRITE), + buildAccountUserMember(newMemberUserId, FmsAccountUserLevelEnum.READ))); + + // 调用 + accountUserService.updateAccountUserList(reqVO, founderUserId); + + // 断言首次替换 + assertEquals(FmsAccountUserLevelEnum.WRITE.getLevel(), accountUserMapper + .selectByAccountSetIdAndUserId(accountSetId, oldMemberUserId).getLevel()); + FmsAccountUserDO newMember = accountUserMapper.selectByAccountSetIdAndUserId( + accountSetId, newMemberUserId); + assertNotNull(newMember); + assertFalse(newMember.getDefaultStatus()); + assertFalse(newMember.getFounder()); + assertEquals(FmsAccountUserLevelEnum.READ.getLevel(), newMember.getLevel()); + verify(accountSetService).validateAccountSetOwnerPermission(accountSetId, founderUserId); + verify(adminUserApi).validateUserList(new HashSet<>(Arrays.asList(oldMemberUserId, newMemberUserId))); + + // 调用,并断言被移出的成员可以再次加入 + reqVO.setMembers(Arrays.asList( + buildAccountUserMember(founderUserId, FmsAccountUserLevelEnum.OWNER), + buildAccountUserMember(oldMemberUserId, FmsAccountUserLevelEnum.READ))); + accountUserService.updateAccountUserList(reqVO, founderUserId); + assertEquals(FmsAccountUserLevelEnum.READ.getLevel(), accountUserMapper + .selectByAccountSetIdAndUserId(accountSetId, oldMemberUserId).getLevel()); + assertNull(accountUserMapper.selectByAccountSetIdAndUserId(accountSetId, newMemberUserId)); + + // 调用,并断言逻辑删除的成员可以再次加入 + reqVO.setMembers(Arrays.asList( + buildAccountUserMember(founderUserId, FmsAccountUserLevelEnum.OWNER), + buildAccountUserMember(oldMemberUserId, FmsAccountUserLevelEnum.READ), + buildAccountUserMember(newMemberUserId, FmsAccountUserLevelEnum.WRITE))); + accountUserService.updateAccountUserList(reqVO, founderUserId); + assertEquals(FmsAccountUserLevelEnum.WRITE.getLevel(), accountUserMapper + .selectByAccountSetIdAndUserId(accountSetId, newMemberUserId).getLevel()); + } + + // ========== 随机对象 ========== + + private FmsAccountUserUpdateReqVO.Member buildAccountUserMember( + Long userId, FmsAccountUserLevelEnum level) { + return new FmsAccountUserUpdateReqVO.Member().setUserId(userId).setLevel(level.getLevel()); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationServiceImplTest.java new file mode 100644 index 0000000..3b9859b --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryCombinationServiceImplTest.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAuxiliaryCombinationMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import java.util.Collections; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Import(FmsAuxiliaryCombinationServiceImpl.class) +public class FmsAuxiliaryCombinationServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsAuxiliaryCombinationServiceImpl auxiliaryCombinationService; + @Resource + private FmsAuxiliaryCombinationMapper auxiliaryCombinationMapper; + + @Test + public void testDeleteAuxiliaryCombinationByAuxiliaryItemIds() { + // mock 数据 + FmsAuxiliaryCombinationDO.AuxiliaryItem deletedItem = FmsAuxiliaryCombinationDO.AuxiliaryItem.builder() + .typeId(31L).itemId(41L).build(); + FmsAuxiliaryCombinationDO deletedCombination = new FmsAuxiliaryCombinationDO().setAccountSetId(1L) + .setSubjectId(101L).setItems(Collections.singletonList(deletedItem)); + auxiliaryCombinationMapper.insert(deletedCombination); + FmsAuxiliaryCombinationDO retainedCombination = new FmsAuxiliaryCombinationDO().setAccountSetId(1L) + .setSubjectId(101L).setItems(Collections.singletonList( + FmsAuxiliaryCombinationDO.AuxiliaryItem.builder().typeId(31L).itemId(42L).build())); + auxiliaryCombinationMapper.insert(retainedCombination); + + // 调用 + auxiliaryCombinationService.deleteAuxiliaryCombinationByAuxiliaryItemIds( + 1L, Collections.singletonList(41L)); + + // 断言 + assertNull(auxiliaryCombinationMapper.selectById(deletedCombination.getId())); + assertPojoEquals(retainedCombination, + auxiliaryCombinationMapper.selectById(retainedCombination.getId()), "deleted"); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemServiceImplTest.java new file mode 100644 index 0000000..9e6b702 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryItemServiceImplTest.java @@ -0,0 +1,265 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAuxiliaryItemMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsAuxiliaryItemServiceImpl.class) +public class FmsAuxiliaryItemServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsAuxiliaryItemServiceImpl auxiliaryItemService; + @Resource + private FmsAuxiliaryItemMapper auxiliaryItemMapper; + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsAuxiliaryTypeService auxiliaryTypeService; + @MockitoBean + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + @MockitoBean + private FmsVoucherService voucherService; + @MockitoBean + private FmsVoucherTemplateService voucherTemplateService; + @MockitoBean + private FmsInitialBalanceService initialBalanceService; + + @Test + public void testCreateAuxiliaryItem_success() { + // mock 方法 + mockAuxiliaryType(1L, 11L); + // 准备参数 + FmsAuxiliaryItemSaveReqVO reqVO = buildSaveReqVO(1L, 11L, "KH001", "测试客户"); + + // 调用 + Long auxiliaryItemId = auxiliaryItemService.createAuxiliaryItem(reqVO, 10L); + + // 断言 + FmsAuxiliaryItemDO auxiliaryItem = auxiliaryItemMapper.selectById(auxiliaryItemId); + assertEquals("KH001", auxiliaryItem.getCode()); + assertEquals("测试客户", auxiliaryItem.getName()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), auxiliaryItem.getStatus()); + assertEquals(11L, auxiliaryItem.getAuxiliaryTypeId()); + verify(accountSetService).validateAccountSetWritePermission(1L, 10L); + } + + @Test + public void testCreateAuxiliaryItem_codeDuplicate() { + // mock 数据 + mockAuxiliaryType(1L, 11L); + auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH001", "原客户")); + // 准备参数 + FmsAuxiliaryItemSaveReqVO reqVO = buildSaveReqVO(1L, 11L, "KH001", "新客户"); + + // 调用,并断言 + assertServiceException(() -> auxiliaryItemService.createAuxiliaryItem(reqVO, 10L), + AUXILIARY_ITEM_CODE_DUPLICATE); + } + + @Test + public void testDeleteAuxiliaryItemList_success() { + // mock 数据 + FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "测试客户"); + auxiliaryItemMapper.insert(auxiliaryItem); + + // 调用 + auxiliaryItemService.deleteAuxiliaryItemList( + 1L, Collections.singletonList(auxiliaryItem.getId()), 10L); + + // 断言 + assertNull(auxiliaryItemMapper.selectById(auxiliaryItem.getId())); + verify(auxiliaryCombinationService).deleteAuxiliaryCombinationByAuxiliaryItemIds( + eq(1L), anyCollection()); + } + + @Test + public void testDeleteAuxiliaryItemList_voucherTemplateInUse() { + // mock 数据 + FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "测试客户"); + auxiliaryItemMapper.insert(auxiliaryItem); + // mock 方法 + when(voucherTemplateService.getVoucherTemplateCountByAuxiliaryItemIds( + eq(1L), anyCollection())).thenReturn(1L); + + // 调用,并断言 + assertServiceException(() -> auxiliaryItemService.deleteAuxiliaryItemList( + 1L, Collections.singletonList(auxiliaryItem.getId()), 10L), + AUXILIARY_ITEM_VOUCHER_TEMPLATE_IN_USE, 1L); + } + + @Test + public void testUpdateAuxiliaryItemStatus_success() { + // mock 数据 + FmsAuxiliaryItemDO auxiliaryItem = buildAuxiliaryItem(1L, 11L, "KH001", "测试客户"); + auxiliaryItemMapper.insert(auxiliaryItem); + + // 调用 + auxiliaryItemService.updateAuxiliaryItemStatus(1L, auxiliaryItem.getId(), + CommonStatusEnum.DISABLE.getStatus(), 10L); + + // 断言 + assertEquals(CommonStatusEnum.DISABLE.getStatus(), + auxiliaryItemMapper.selectById(auxiliaryItem.getId()).getStatus()); + verify(accountSetService).validateAccountSetWritePermission(1L, 10L); + } + + @Test + public void testGetAuxiliaryItemPage_search() { + // mock 数据 + mockAuxiliaryType(1L, 11L); + auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH001", "上海测试")); + auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH002", "北京云财务")); + // 准备参数 + FmsAuxiliaryItemPageReqVO reqVO = new FmsAuxiliaryItemPageReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setAuxiliaryTypeId(11L); + reqVO.setSearch("测试"); + + // 调用 + PageResult pageResult = auxiliaryItemService + .getAuxiliaryItemPage(reqVO, 10L); + + // 断言 + assertEquals(1L, pageResult.getTotal()); + assertEquals("KH001", CollUtil.getFirst(pageResult.getList()).getCode()); + } + + @Test + public void testGetAuxiliaryItemListByAccountSetIdAndAuxiliaryTypeIdAndStatus() { + // mock 数据 + mockAuxiliaryType(1L, 11L); + auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH002", "北京客户")); + auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH001", "上海客户")); + auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH003", "停用客户") + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + + // 调用 + List result = auxiliaryItemService + .getAuxiliaryItemListByAccountSetIdAndAuxiliaryTypeIdAndStatus( + 1L, 11L, CommonStatusEnum.ENABLE.getStatus(), 10L); + + // 断言 + assertEquals(2, result.size()); + assertEquals("KH001", result.get(0).getCode()); + assertEquals("KH002", result.get(1).getCode()); + } + + @Test + public void testImportAuxiliaryItemList_empty() { + // 调用,并断言 + assertServiceException(() -> auxiliaryItemService.importAuxiliaryItemList( + 1L, 11L, Collections.emptyList(), 10L), AUXILIARY_ITEM_IMPORT_LIST_IS_EMPTY); + } + + @Test + public void testImportAuxiliaryItemList_partialSuccess() { + // mock 数据 + mockAuxiliaryType(1L, 11L); + auxiliaryItemMapper.insert(buildAuxiliaryItem(1L, 11L, "KH001", "原客户")); + FmsAuxiliaryItemImportExcelVO successRow = buildImportRow("KH002", "新客户"); + successRow.setRemark("重点客户"); + FmsAuxiliaryItemImportExcelVO existRow = buildImportRow("KH001", "重复客户"); + FmsAuxiliaryItemImportExcelVO duplicateRow = buildImportRow("KH002", "文件内重复"); + + // 调用 + FmsAuxiliaryItemImportRespVO result = auxiliaryItemService.importAuxiliaryItemList( + 1L, 11L, Arrays.asList(successRow, existRow, duplicateRow), 10L); + + // 断言 + assertEquals(3, result.getTotalCount()); + assertEquals(Collections.singletonList("KH002"), result.getSuccessItemCodes()); + assertEquals(2, result.getFailureReasons().size()); + FmsAuxiliaryItemDO item = auxiliaryItemMapper.selectByTypeIdAndCode(1L, 11L, "KH002"); + assertEquals("新客户", item.getName()); + assertEquals("重点客户", item.getRemark()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), item.getStatus()); + } + + @Test + public void testImportAuxiliaryItemList_inventory() { + // mock 数据 + when(auxiliaryTypeService.validateAuxiliaryTypeList(eq(1L), anyCollection())) + .thenReturn(Collections.singletonList(new FmsAuxiliaryTypeDO() + .setId(16L).setAccountSetId(1L).setName("存货").setSystemPreset(true) + .setType(FmsAuxiliaryTypeEnum.INVENTORY.getType()))); + FmsAuxiliaryItemImportExcelVO row = buildImportRow("SP001", "测试商品"); + row.setRemark("库存备注"); + row.setSpecification("标准版"); + row.setUnit("台"); + + // 调用 + auxiliaryItemService.importAuxiliaryItemList( + 1L, 16L, Collections.singletonList(row), 10L); + + // 断言 + FmsAuxiliaryItemDO item = auxiliaryItemMapper.selectByTypeIdAndCode(1L, 16L, "SP001"); + assertEquals("库存备注", item.getRemark()); + assertEquals("标准版", item.getSpecification()); + assertEquals("台", item.getUnit()); + } + + // ========== 随机对象 ========== + + private void mockAuxiliaryType(Long accountSetId, Long auxiliaryTypeId) { + when(auxiliaryTypeService.validateAuxiliaryTypeList(eq(accountSetId), anyCollection())) + .thenReturn(Collections.singletonList(new FmsAuxiliaryTypeDO() + .setId(auxiliaryTypeId).setAccountSetId(accountSetId) + .setName("客户").setSystemPreset(true) + .setType(FmsAuxiliaryTypeEnum.CUSTOMER.getType()))); + } + + private FmsAuxiliaryItemSaveReqVO buildSaveReqVO( + Long accountSetId, Long auxiliaryTypeId, String code, String name) { + FmsAuxiliaryItemSaveReqVO reqVO = new FmsAuxiliaryItemSaveReqVO(); + reqVO.setAccountSetId(accountSetId); + reqVO.setAuxiliaryTypeId(auxiliaryTypeId); + reqVO.setCode(code); + reqVO.setName(name); + return reqVO; + } + + private FmsAuxiliaryItemImportExcelVO buildImportRow(String code, String name) { + FmsAuxiliaryItemImportExcelVO row = new FmsAuxiliaryItemImportExcelVO(); + row.setCode(code); + row.setName(name); + return row; + } + + private FmsAuxiliaryItemDO buildAuxiliaryItem( + Long accountSetId, Long auxiliaryTypeId, String code, String name) { + return randomPojo(FmsAuxiliaryItemDO.class, auxiliaryItem -> auxiliaryItem.setId(null) + .setAccountSetId(accountSetId).setAuxiliaryTypeId(auxiliaryTypeId) + .setCode(code).setName(name).setStatus(CommonStatusEnum.ENABLE.getStatus())); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeServiceImplTest.java new file mode 100644 index 0000000..76a92c4 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsAuxiliaryTypeServiceImplTest.java @@ -0,0 +1,170 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype.FmsAuxiliaryTypeSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsAuxiliaryTypeMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsAuxiliaryTypeServiceImpl.class) +public class FmsAuxiliaryTypeServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsAuxiliaryTypeServiceImpl auxiliaryTypeService; + @Resource + private FmsAuxiliaryTypeMapper auxiliaryTypeMapper; + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsAuxiliaryItemService auxiliaryItemService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsVoucherService voucherService; + @MockitoBean + private FmsVoucherTemplateService voucherTemplateService; + @MockitoBean + private FmsInitialBalanceService initialBalanceService; + + @Test + public void testInitializeDefaultTypes_success() { + // 准备参数 + Long accountSetId = 1L; + + // 调用 + auxiliaryTypeService.initializeDefaultTypes(accountSetId); + + // 断言 + List types = auxiliaryTypeMapper.selectListByAccountSetId(accountSetId); + assertEquals(6, types.size()); + assertEquals(FmsAuxiliaryTypeEnum.CUSTOMER.getName(), types.get(0).getName()); + assertEquals(FmsAuxiliaryTypeEnum.INVENTORY.getName(), types.get(5).getName()); + assertTrue(types.stream().allMatch(FmsAuxiliaryTypeDO::getSystemPreset)); + assertTrue(types.stream().allMatch(type -> accountSetId.equals(type.getAccountSetId()))); + assertFalse(types.stream().anyMatch( + type -> FmsAuxiliaryTypeEnum.CUSTOM.getType().equals(type.getType()))); + } + + @Test + public void testGetAuxiliaryTypeIdMap() { + // mock 数据 + Long accountSetId = 1L; + FmsAuxiliaryTypeDO customer = buildAuxiliaryType(accountSetId, "客户", true); + FmsAuxiliaryTypeDO store = buildAuxiliaryType(accountSetId, "门店", false); + auxiliaryTypeMapper.insert(customer); + auxiliaryTypeMapper.insert(store); + + // 调用 + Map auxiliaryTypeIdMap = auxiliaryTypeService.getAuxiliaryTypeIdMap(accountSetId); + + // 断言 + assertEquals(2, auxiliaryTypeIdMap.size()); + assertEquals(customer.getId(), auxiliaryTypeIdMap.get("客户")); + assertEquals(store.getId(), auxiliaryTypeIdMap.get("门店")); + } + + @Test + public void testCreateAuxiliaryType_success() { + // 准备参数 + FmsAuxiliaryTypeSaveReqVO reqVO = new FmsAuxiliaryTypeSaveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setName("门店"); + + // 调用 + Long auxiliaryTypeId = auxiliaryTypeService.createAuxiliaryType(reqVO, 10L); + + // 断言 + FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeMapper.selectById(auxiliaryTypeId); + assertEquals("门店", auxiliaryType.getName()); + assertEquals(FmsAuxiliaryTypeEnum.CUSTOM.getType(), auxiliaryType.getType()); + assertFalse(auxiliaryType.getSystemPreset()); + assertEquals(1L, auxiliaryType.getAccountSetId()); + verify(accountSetService).validateAccountSetWritePermission(1L, 10L); + } + + @Test + public void testCreateAuxiliaryType_nameDuplicate() { + // mock 数据 + auxiliaryTypeMapper.insert(buildAuxiliaryType(1L, "门店", false)); + // 准备参数 + FmsAuxiliaryTypeSaveReqVO reqVO = new FmsAuxiliaryTypeSaveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setName("门店"); + + // 调用,并断言 + assertServiceException(() -> auxiliaryTypeService.createAuxiliaryType(reqVO, 10L), + AUXILIARY_TYPE_NAME_DUPLICATE); + } + + @Test + public void testUpdateAuxiliaryType_systemPreset() { + // mock 数据 + FmsAuxiliaryTypeDO auxiliaryType = buildAuxiliaryType(1L, "客户", true); + auxiliaryTypeMapper.insert(auxiliaryType); + // 准备参数 + FmsAuxiliaryTypeSaveReqVO reqVO = new FmsAuxiliaryTypeSaveReqVO(); + reqVO.setId(auxiliaryType.getId()); + reqVO.setAccountSetId(1L); + reqVO.setName("客户资料"); + + // 调用,并断言 + assertServiceException(() -> auxiliaryTypeService.updateAuxiliaryType(reqVO, 10L), + AUXILIARY_TYPE_SYSTEM_NOT_EDITABLE); + } + + @Test + public void testDeleteAuxiliaryType_hasItem() { + // mock 数据 + FmsAuxiliaryTypeDO auxiliaryType = buildAuxiliaryType(1L, "门店", false); + auxiliaryTypeMapper.insert(auxiliaryType); + // mock 方法 + when(auxiliaryItemService.getAuxiliaryItemCountByAuxiliaryTypeId( + 1L, auxiliaryType.getId())).thenReturn(1L); + + // 调用,并断言 + assertServiceException(() -> auxiliaryTypeService.deleteAuxiliaryType( + 1L, auxiliaryType.getId(), 10L), AUXILIARY_TYPE_HAS_ITEM, 1L); + } + + @Test + public void testDeleteAuxiliaryType_inUse() { + // mock 数据 + FmsAuxiliaryTypeDO auxiliaryType = buildAuxiliaryType(1L, "门店", false); + auxiliaryTypeMapper.insert(auxiliaryType); + when(subjectService.getSubjectCountByAuxiliaryTypeId(1L, auxiliaryType.getId())) + .thenReturn(1L); + + // 调用,并断言 + assertServiceException(() -> auxiliaryTypeService.deleteAuxiliaryType( + 1L, auxiliaryType.getId(), 10L), AUXILIARY_TYPE_SUBJECT_IN_USE, 1L); + } + + // ========== 随机对象 ========== + + private FmsAuxiliaryTypeDO buildAuxiliaryType(Long accountSetId, String name, boolean systemPreset) { + return randomPojo(FmsAuxiliaryTypeDO.class, auxiliaryType -> auxiliaryType.setId(null) + .setName(name).setSystemPreset(systemPreset).setAccountSetId(accountSetId) + .setType(systemPreset + ? FmsAuxiliaryTypeEnum.CUSTOMER.getType() : FmsAuxiliaryTypeEnum.CUSTOM.getType())); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyServiceImplTest.java new file mode 100644 index 0000000..10b6b48 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsCurrencyServiceImplTest.java @@ -0,0 +1,232 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency.FmsCurrencySaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsCurrencyMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsCurrencyPresetEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_IN_USE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_NOT_EXISTS; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.CURRENCY_STANDARD_NOT_DELETABLE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@Import(FmsCurrencyServiceImpl.class) +public class FmsCurrencyServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsCurrencyServiceImpl currencyService; + @Resource + private FmsCurrencyMapper currencyMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + + @Test + public void testInitializeStandardCurrency_success() { + // 准备参数 + Long accountSetId = 1L; + + // 调用 + FmsCurrencyDO currency = currencyService.initializeStandardCurrency( + accountSetId, FmsCurrencyPresetEnum.RMB); + + // 断言 + FmsCurrencyDO dbCurrency = currencyMapper.selectById(currency.getId()); + assertEquals(accountSetId, dbCurrency.getAccountSetId()); + assertEquals(FmsCurrencyPresetEnum.RMB.getCode(), dbCurrency.getCode()); + assertEquals(FmsCurrencyPresetEnum.RMB.getName(), dbCurrency.getName()); + assertEquals(0, BigDecimal.ONE.compareTo(dbCurrency.getExchangeRate())); + assertTrue(dbCurrency.getStandard()); + } + + @Test + public void testCreateCurrency_success() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + // 准备参数 + FmsCurrencySaveReqVO reqVO = buildSaveReqVO(null, accountSetId, + "USD", "美元", new BigDecimal("7.120000")); + + // 调用 + Long currencyId = currencyService.createCurrency(reqVO, 10L); + + // 断言 + FmsCurrencyDO currency = currencyMapper.selectById(currencyId); + assertEquals("USD", currency.getCode()); + assertEquals("美元", currency.getName()); + assertEquals(0, new BigDecimal("7.120000").compareTo(currency.getExchangeRate())); + assertEquals(Boolean.FALSE, currency.getStandard()); + } + + @Test + public void testCreateCurrency_codeDuplicate() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + currencyMapper.insert(buildCurrencyDO(accountSetId, "USD", "美元", false)); + // 准备参数 + FmsCurrencySaveReqVO reqVO = buildSaveReqVO(null, accountSetId, + "USD", "美金", new BigDecimal("7.100000")); + + // 调用,并断言 + assertServiceException(() -> currencyService.createCurrency(reqVO, 10L), CURRENCY_CODE_DUPLICATE); + } + + @Test + public void testUpdateCurrency_standardFieldsProtected() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsCurrencyDO currency = buildCurrencyDO(accountSetId, "RMB", "人民币", true); + currency.setExchangeRate(BigDecimal.ONE); + currencyMapper.insert(currency); + // 准备参数 + FmsCurrencySaveReqVO reqVO = buildSaveReqVO(currency.getId(), accountSetId, + "CNY", "人民币本位币", new BigDecimal("2.000000")); + + // 调用 + currencyService.updateCurrency(reqVO, 10L); + + // 断言 + FmsCurrencyDO updateCurrency = currencyMapper.selectById(currency.getId()); + assertEquals("RMB", updateCurrency.getCode()); + assertEquals("人民币本位币", updateCurrency.getName()); + assertEquals(0, BigDecimal.ONE.compareTo(updateCurrency.getExchangeRate())); + assertTrue(updateCurrency.getStandard()); + } + + @Test + public void testDeleteCurrency_standardNotDeletable() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsCurrencyDO currency = buildCurrencyDO(accountSetId, "RMB", "人民币", true); + currencyMapper.insert(currency); + + // 调用,并断言 + assertServiceException(() -> currencyService.deleteCurrency( + accountSetId, currency.getId(), 10L), CURRENCY_STANDARD_NOT_DELETABLE); + } + + @Test + public void testDeleteCurrency_success() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsCurrencyDO currency = buildCurrencyDO(accountSetId, "USD", "美元", false); + currencyMapper.insert(currency); + + // 调用 + currencyService.deleteCurrency(accountSetId, currency.getId(), 10L); + + // 断言 + assertNull(currencyMapper.selectById(currency.getId())); + } + + @Test + public void testDeleteCurrency_inUse() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsCurrencyDO currency = buildCurrencyDO(accountSetId, "USD", "美元", false); + currencyMapper.insert(currency); + when(subjectService.getSubjectCountByCurrencyId(accountSetId, currency.getId())).thenReturn(1L); + + // 调用,并断言 + assertServiceException(() -> currencyService.deleteCurrency( + accountSetId, currency.getId(), 10L), CURRENCY_IN_USE); + } + + @Test + public void testGetCurrencyList_standardFirst() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + currencyMapper.insert(buildCurrencyDO(accountSetId, "USD", "美元", false)); + currencyMapper.insert(buildCurrencyDO(accountSetId, "RMB", "人民币", true)); + + // 调用 + List currencies = currencyService.getCurrencyList(accountSetId, 10L); + + // 断言 + assertEquals(2, currencies.size()); + assertEquals("RMB", currencies.get(0).getCode()); + assertEquals("USD", currencies.get(1).getCode()); + } + + @Test + public void testValidateCurrencyList_success() { + // mock 数据 + Long accountSetId = 1L; + FmsCurrencyDO usd = buildCurrencyDO(accountSetId, "USD", "美元", false); + FmsCurrencyDO eur = buildCurrencyDO(accountSetId, "EUR", "欧元", false); + currencyMapper.insert(usd); + currencyMapper.insert(eur); + + // 调用 + List currencies = currencyService.validateCurrencyList( + accountSetId, Arrays.asList(eur.getId(), usd.getId())); + + // 断言 + assertEquals(2, currencies.size()); + assertEquals(new HashSet<>(Arrays.asList(eur.getId(), usd.getId())), + new HashSet<>(Arrays.asList(currencies.get(0).getId(), currencies.get(1).getId()))); + } + + @Test + public void testValidateCurrencyList_duplicateId() { + // mock 数据 + Long accountSetId = 1L; + FmsCurrencyDO currency = buildCurrencyDO(accountSetId, "USD", "美元", false); + currencyMapper.insert(currency); + + // 调用,并断言 + assertServiceException(() -> currencyService.validateCurrencyList( + accountSetId, Arrays.asList(currency.getId(), currency.getId())), CURRENCY_NOT_EXISTS); + } + + // ========== 随机对象 ========== + + private FmsCurrencyDO buildCurrencyDO(Long accountSetId, String code, String name, boolean standard) { + return randomPojo(FmsCurrencyDO.class, currency -> currency.setId(null) + .setAccountSetId(accountSetId).setCode(code).setName(name) + .setExchangeRate(new BigDecimal("7.120000")).setStandard(standard)); + } + + private FmsCurrencySaveReqVO buildSaveReqVO(Long id, Long accountSetId, String code, + String name, BigDecimal exchangeRate) { + return randomPojo(FmsCurrencySaveReqVO.class, reqVO -> reqVO.setId(id) + .setAccountSetId(accountSetId).setCode(code).setName(name).setExchangeRate(exchangeRate)); + } + + private void mockAccountSetAccess(Long accountSetId) { + when(accountSetService.validateAccountSetReadPermission(accountSetId, 10L)) + .thenReturn(new FmsAccountSetDO().setId(accountSetId)); + when(accountSetService.validateAccountSetWritePermission(accountSetId, 10L)) + .thenReturn(new FmsAccountSetDO().setId(accountSetId)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestServiceImplTest.java new file mode 100644 index 0000000..96ad9c5 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsDigestServiceImplTest.java @@ -0,0 +1,120 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest.FmsDigestSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsDigestDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsDigestMapper; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.DIGEST_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +@Import(FmsDigestServiceImpl.class) +public class FmsDigestServiceImplTest extends BaseDbUnitTest { + + private static final Long USER_ID = 10L; + + @Resource + private FmsDigestServiceImpl digestService; + @Resource + private FmsDigestMapper digestMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + + @Test + public void testCreateDigest_success() { + // 准备参数 + FmsDigestSaveReqVO reqVO = buildSaveReqVO(null, 1L, "购买办公用品"); + + // 调用 + Long digestId = digestService.createDigest(reqVO, USER_ID); + + // 断言 + FmsDigestDO digest = digestMapper.selectById(digestId); + assertEquals("购买办公用品", digest.getContent()); + assertEquals(1L, digest.getAccountSetId()); + verify(accountSetService).validateAccountSetWritePermission(1L, USER_ID); + } + + @Test + public void testUpdateDigest_success() { + // mock 数据 + FmsDigestDO digest = new FmsDigestDO().setAccountSetId(1L).setContent("购买办公用品"); + digestMapper.insert(digest); + // 准备参数 + FmsDigestSaveReqVO reqVO = buildSaveReqVO(digest.getId(), 1L, "支付办公用品款"); + + // 调用 + digestService.updateDigest(reqVO, USER_ID); + + // 断言 + assertEquals("支付办公用品款", digestMapper.selectById(digest.getId()).getContent()); + verify(accountSetService).validateAccountSetWritePermission(1L, USER_ID); + } + + @Test + public void testUpdateDigest_wrongAccountSet() { + // mock 数据 + FmsDigestDO digest = new FmsDigestDO().setAccountSetId(1L).setContent("购买办公用品"); + digestMapper.insert(digest); + // 准备参数 + FmsDigestSaveReqVO reqVO = buildSaveReqVO(digest.getId(), 2L, "支付办公用品款"); + + // 调用,并断言 + assertServiceException(() -> digestService.updateDigest(reqVO, USER_ID), DIGEST_NOT_EXISTS); + verify(accountSetService).validateAccountSetWritePermission(2L, USER_ID); + } + + @Test + public void testDeleteDigest_success() { + // mock 数据 + FmsDigestDO digest = new FmsDigestDO().setAccountSetId(1L).setContent("购买办公用品"); + digestMapper.insert(digest); + + // 调用 + digestService.deleteDigest(1L, digest.getId(), USER_ID); + + // 断言 + assertNull(digestMapper.selectById(digest.getId())); + verify(accountSetService).validateAccountSetWritePermission(1L, USER_ID); + } + + @Test + public void testGetDigestList_accountSetAndSort() { + // mock 数据 + FmsDigestDO first = new FmsDigestDO().setAccountSetId(1L).setContent("购买办公用品"); + digestMapper.insert(first); + FmsDigestDO second = new FmsDigestDO().setAccountSetId(1L).setContent("支付员工报销款"); + digestMapper.insert(second); + digestMapper.insert(new FmsDigestDO().setAccountSetId(2L).setContent("其他账套摘要")); + + // 调用 + List list = digestService.getDigestList(1L, USER_ID); + + // 断言 + assertEquals(2, list.size()); + assertEquals(second.getId(), CollUtil.getFirst(list).getId()); + verify(accountSetService).validateAccountSetReadPermission(1L, USER_ID); + } + + // ========== 随机对象 ========== + + private FmsDigestSaveReqVO buildSaveReqVO(Long id, Long accountSetId, String content) { + FmsDigestSaveReqVO reqVO = new FmsDigestSaveReqVO(); + reqVO.setId(id); + reqVO.setAccountSetId(accountSetId); + reqVO.setContent(content); + return reqVO; + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterServiceImplTest.java new file mode 100644 index 0000000..d02ab09 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsFinanceParameterServiceImplTest.java @@ -0,0 +1,155 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter.FmsFinanceParameterUpdateReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsFinanceParameterMapper; +import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_SETTINGS_RULE_INVALID; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_SETTINGS_RULE_SHRINK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@Import(FmsFinanceParameterServiceImpl.class) +public class FmsFinanceParameterServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsFinanceParameterServiceImpl financeParameterService; + @Resource + private FmsFinanceParameterMapper financeParameterMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + + @Test + public void testInitializeFinanceParameter_success() { + // 准备参数 + Long accountSetId = randomLongId(); + FmsAccountSetInitializeReqVO reqVO = new FmsAccountSetInitializeReqVO() + .setLevel(4).setSubjectCodeRule("4-2-2-2") + .setLedgerBalanceMode(FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode()); + + // 调用 + financeParameterService.initializeFinanceParameter(accountSetId, reqVO); + + // 断言 + FmsFinanceParameterDO financeParameter = financeParameterMapper.selectByAccountSetId(accountSetId); + assertEquals(reqVO.getLevel(), financeParameter.getLevel()); + assertEquals(reqVO.getSubjectCodeRule(), financeParameter.getSubjectCodeRule()); + assertEquals(reqVO.getLedgerBalanceMode(), financeParameter.getLedgerBalanceMode()); + assertFalse(financeParameter.getVoucherReviewRequired()); + assertEquals(FmsFinanceParameterDO.DEFAULT_DEFICIT_CHECK, financeParameter.getDeficitCheck()); + assertEquals(FmsFinanceParameterDO.DEFAULT_ASSET_PERIOD_LOCKED, + financeParameter.getAssetPeriodLocked()); + } + + @Test + public void testInitializeFinanceParameter_ruleInvalid() { + // 准备参数 + FmsAccountSetInitializeReqVO reqVO = new FmsAccountSetInitializeReqVO() + .setLevel(2).setSubjectCodeRule("4-1") + .setLedgerBalanceMode(FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode()); + + // 调用,并断言异常 + assertServiceException(() -> financeParameterService.initializeFinanceParameter( + randomLongId(), reqVO), ACCOUNT_SET_SETTINGS_RULE_INVALID); + } + + @Test + public void testGetFinanceParameter_notInitialized() { + // mock 方法 + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L).setInitialized(false)); + + // 调用,并断言未初始化账套没有财务参数 + assertNull(financeParameterService.getFinanceParameter(1L, 10L)); + verify(accountSetService).validateAccountSetReadPermission(1L, 10L); + } + + @Test + public void testConvertStandardSubjectCode() { + // 调用,并断言 + assertEquals("40404", financeParameterService.convertStandardSubjectCode("4404", "5-3-2-2")); + assertEquals("10001001", financeParameterService.convertStandardSubjectCode("100101", "5-3-2-2")); + } + + @Test + public void testUpdateFinanceParameter_success() { + // mock 数据 + Long accountSetId = randomLongId(); + FmsFinanceParameterDO financeParameter = buildFinanceParameter(accountSetId); + financeParameterMapper.insert(financeParameter); + when(accountSetService.validateAccountSetWritePermission(accountSetId, 10L)) + .thenReturn(new FmsAccountSetDO().setId(accountSetId).setInitialized(true)); + // 准备参数 + FmsFinanceParameterUpdateReqVO reqVO = new FmsFinanceParameterUpdateReqVO() + .setAccountSetId(accountSetId).setStandard(1).setLevel(5).setSubjectCodeRule("5-3-2-2-2") + .setLedgerBalanceMode(FmsLedgerBalanceModeEnum.OPPOSITE_TO_SUBJECT.getMode()) + .setVoucherReviewRequired(false); + + // 调用 + financeParameterService.updateFinanceParameter(reqVO, 10L); + + // 断言 + FmsFinanceParameterDO dbParameter = financeParameterMapper.selectById(financeParameter.getId()); + assertEquals(reqVO.getLevel(), dbParameter.getLevel()); + assertEquals(reqVO.getSubjectCodeRule(), dbParameter.getSubjectCodeRule()); + assertEquals(FmsFinanceParameterDO.DEFAULT_DEFICIT_CHECK, dbParameter.getDeficitCheck()); + verify(subjectService).expandSubjectCodes(accountSetId, + Arrays.asList(4, 2, 2, 2), Arrays.asList(5, 3, 2, 2, 2)); + verify(accountSetService).updateAccountSetStandard(accountSetId, reqVO.getStandard(), 10L); + } + + @Test + public void testUpdateFinanceParameter_ruleShrink() { + // mock 数据 + Long accountSetId = randomLongId(); + financeParameterMapper.insert(buildFinanceParameter(accountSetId)); + when(accountSetService.validateAccountSetWritePermission(accountSetId, 10L)) + .thenReturn(new FmsAccountSetDO().setId(accountSetId).setInitialized(true)); + // 准备参数 + FmsFinanceParameterUpdateReqVO reqVO = new FmsFinanceParameterUpdateReqVO() + .setAccountSetId(accountSetId).setStandard(1).setLevel(4).setSubjectCodeRule("3-2-2-2") + .setLedgerBalanceMode(FmsLedgerBalanceModeEnum.SAME_AS_SUBJECT.getMode()) + .setVoucherReviewRequired(true); + + // 调用,并断言异常 + assertServiceException(() -> financeParameterService.updateFinanceParameter(reqVO, 10L), + ACCOUNT_SET_SETTINGS_RULE_SHRINK); + verifyNoInteractions(subjectService); + } + + // ========== 随机对象 ========== + + private FmsFinanceParameterDO buildFinanceParameter(Long accountSetId) { + return randomPojo(FmsFinanceParameterDO.class, financeParameter -> financeParameter.setId(null) + .setAccountSetId(accountSetId) + .setLevel(FmsFinanceParameterDO.DEFAULT_LEVEL) + .setSubjectCodeRule(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE) + .setLedgerBalanceMode(FmsFinanceParameterDO.DEFAULT_LEDGER_BALANCE_MODE) + .setVoucherReviewRequired(FmsFinanceParameterDO.DEFAULT_VOUCHER_REVIEW_REQUIRED) + .setDeficitCheck(FmsFinanceParameterDO.DEFAULT_DEFICIT_CHECK) + .setAssetPeriodLocked(FmsFinanceParameterDO.DEFAULT_ASSET_PERIOD_LOCKED)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceServiceImplTest.java new file mode 100644 index 0000000..9164ea4 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsInitialBalanceServiceImplTest.java @@ -0,0 +1,338 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsInitialBalanceDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsInitialBalanceMapper; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryCombinationService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryItemService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_PERIOD_CLOSED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_SUBJECT_NOT_LEAF; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsInitialBalanceServiceImpl.class) +public class FmsInitialBalanceServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsInitialBalanceServiceImpl initialBalanceService; + @Resource + private FmsInitialBalanceMapper initialBalanceMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsAuxiliaryItemService auxiliaryItemService; + @MockitoBean + private FmsAuxiliaryTypeService auxiliaryTypeService; + @MockitoBean + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + @MockitoBean + private FmsClosingPeriodService closingPeriodService; + + @Test + public void testGetInitialBalanceList_aggregateParent() { + // mock 数据 + Long accountSetId = 1L; + FmsSubjectDO parent = buildSubject(10L, null, FmsDebitCreditDirectionEnum.DEBIT.getType()); + FmsSubjectDO child = buildSubject(11L, parent.getId(), FmsDebitCreditDirectionEnum.DEBIT.getType()); + mockAccountSetAndSubjects(accountSetId, Arrays.asList(parent, child)); + initialBalanceMapper.insert(buildBalance(accountSetId, child.getId(), "120.00")); + + // 调用 + List result = initialBalanceService.getInitialBalanceList( + accountSetId, FmsSubjectTypeEnum.ASSET.getType(), 10L); + + // 断言:平铺列表父级在前,父级余额由末级科目汇总 + assertEquals(2, result.size()); + assertEquals(parent.getId(), result.get(0).getSubjectId()); + assertEquals(new BigDecimal("120.00"), result.get(0).getOpeningAmount()); + assertEquals(child.getId(), result.get(1).getSubjectId()); + assertEquals(new BigDecimal("120.00"), result.get(1).getOpeningAmount()); + } + + @Test + public void testSaveInitialBalance_success() { + // mock 数据 + Long accountSetId = 1L; + FmsSubjectDO subject = buildSubject(11L, null, FmsDebitCreditDirectionEnum.DEBIT.getType()); + mockAccountSetAndSubjects(accountSetId, Collections.singletonList(subject)); + when(closingPeriodService.getCurrentMonth(eq(accountSetId), any(LocalDateTime.class))) + .thenReturn(YearMonth.of(2026, 3)); + // 准备参数 + FmsInitialBalanceSaveReqVO reqVO = new FmsInitialBalanceSaveReqVO(); + reqVO.setAccountSetId(accountSetId).setBalances(Collections.singletonList( + buildSaveBalance(subject.getId(), "1000.00", "200.00", "100.00", "900.00"))); + + // 调用 + initialBalanceService.saveInitialBalance(reqVO, 10L); + + // 断言 + FmsInitialBalanceDO balance = initialBalanceMapper.selectByAccountSetIdAndSubjectId( + accountSetId, subject.getId()); + assertEquals(new BigDecimal("1000.00"), balance.getOpeningAmount()); + assertEquals(new BigDecimal("200.00"), balance.getYearDebitAmount()); + assertEquals(new BigDecimal("100.00"), balance.getYearCreditAmount()); + assertEquals(new BigDecimal("900.00"), balance.getYearOpeningAmount()); + verify(accountSetService).validateAccountSetWritePermission(accountSetId, 10L); + } + + @Test + public void testSaveInitialBalance_subjectNotLeaf() { + // mock 数据 + Long accountSetId = 1L; + FmsSubjectDO parent = buildSubject(10L, null, FmsDebitCreditDirectionEnum.DEBIT.getType()); + FmsSubjectDO child = buildSubject(11L, parent.getId(), FmsDebitCreditDirectionEnum.DEBIT.getType()); + mockAccountSetAndSubjects(accountSetId, Arrays.asList(parent, child)); + when(closingPeriodService.getCurrentMonth(eq(accountSetId), any(LocalDateTime.class))) + .thenReturn(YearMonth.of(2026, 3)); + // 准备参数 + FmsInitialBalanceSaveReqVO reqVO = new FmsInitialBalanceSaveReqVO(); + reqVO.setAccountSetId(accountSetId).setBalances(Collections.singletonList( + buildSaveBalance(parent.getId(), "100.00", "0", "0", "100.00"))); + + // 调用,并断言 + assertServiceException(() -> initialBalanceService.saveInitialBalance(reqVO, 10L), + INITIAL_BALANCE_SUBJECT_NOT_LEAF); + } + + @Test + public void testSaveInitialBalance_periodClosed() { + // mock 数据 + Long accountSetId = 1L; + FmsSubjectDO subject = buildSubject(11L, null, FmsDebitCreditDirectionEnum.DEBIT.getType()); + mockAccountSetAndSubjects(accountSetId, Collections.singletonList(subject)); + // 账套已结账,当前期间晚于启用期间 + when(closingPeriodService.getCurrentMonth(eq(accountSetId), any(LocalDateTime.class))) + .thenReturn(YearMonth.of(2026, 4)); + // 准备参数 + FmsInitialBalanceSaveReqVO reqVO = new FmsInitialBalanceSaveReqVO(); + reqVO.setAccountSetId(accountSetId).setBalances(Collections.singletonList( + buildSaveBalance(subject.getId(), "100.00", "0", "0", "100.00"))); + + // 调用,并断言 + assertServiceException(() -> initialBalanceService.saveInitialBalance(reqVO, 10L), + INITIAL_BALANCE_PERIOD_CLOSED); + } + + @Test + public void testGetTrialBalance_balanced() { + // mock 数据 + Long accountSetId = 1L; + FmsSubjectDO debit = buildSubject(10L, null, FmsDebitCreditDirectionEnum.DEBIT.getType()); + FmsSubjectDO credit = buildSubject(20L, null, FmsDebitCreditDirectionEnum.CREDIT.getType()); + mockAccountSetAndSubjects(accountSetId, Arrays.asList(debit, credit)); + initialBalanceMapper.insert(buildBalance(accountSetId, debit.getId(), "1000.00")); + initialBalanceMapper.insert(buildBalance(accountSetId, credit.getId(), "1000.00")); + + // 调用 + FmsTrialBalanceRespVO result = initialBalanceService.getTrialBalance(accountSetId, 10L); + + // 断言 + assertTrue(result.getBalanced()); + assertEquals(new BigDecimal("1000.00"), result.getOpeningDebitAmount()); + assertEquals(new BigDecimal("1000.00"), result.getOpeningCreditAmount()); + assertEquals(BigDecimal.ZERO.setScale(2), result.getOpeningDifferenceAmount()); + } + + @Test + public void testImportInitialBalance_success() { + // mock 数据 + Long accountSetId = 1L; + FmsSubjectDO subject = buildSubject(11L, null, FmsDebitCreditDirectionEnum.DEBIT.getType()); + mockAccountSetAndSubjects(accountSetId, Collections.singletonList(subject)); + when(closingPeriodService.getCurrentMonth(eq(accountSetId), any(LocalDateTime.class))) + .thenReturn(YearMonth.of(2026, 3)); + when(auxiliaryTypeService.getAuxiliaryTypeList(accountSetId, 10L)) + .thenReturn(Collections.emptyList()); + when(auxiliaryItemService.getAuxiliaryItemListByAccountSetId(accountSetId, 10L)) + .thenReturn(Collections.emptyList()); + // 准备参数 + FmsInitialBalanceExcelVO row = new FmsInitialBalanceExcelVO().setRowNumber(3) + .setSubjectCode(subject.getCode()).setDirectionName("借") + .setOpeningAmount(new BigDecimal("1000.00")) + .setOpeningQuantity(new BigDecimal("10.0000")) + .setYearDebitAmount(new BigDecimal("200.00")) + .setYearDebitQuantity(new BigDecimal("2.0000")) + .setYearCreditAmount(new BigDecimal("100.00")) + .setYearCreditQuantity(new BigDecimal("1.0000")); + + // 调用 + int count = initialBalanceService.importInitialBalance( + accountSetId, Collections.singletonList(row), 10L); + + // 断言 + assertEquals(1, count); + FmsInitialBalanceDO balance = initialBalanceMapper.selectByAccountSetIdAndSubjectId( + accountSetId, subject.getId()); + assertEquals(new BigDecimal("900.00"), balance.getYearOpeningAmount()); + assertEquals(new BigDecimal("9.0000"), balance.getYearOpeningQuantity()); + verify(accountSetService).validateAccountSetWritePermission(accountSetId, 10L); + } + + @Test + public void testImportInitialBalance_auxiliaryEmbeddedJson() { + // mock 数据 + Long accountSetId = 1L; + Long auxiliaryTypeId = 21L; + FmsSubjectDO subject = buildSubject(11L, null, FmsDebitCreditDirectionEnum.DEBIT.getType()) + .setAuxiliaryTypeIds(Collections.singletonList(auxiliaryTypeId)); + mockAccountSetAndSubjects(accountSetId, Collections.singletonList(subject)); + when(closingPeriodService.getCurrentMonth(eq(accountSetId), any(LocalDateTime.class))) + .thenReturn(YearMonth.of(2026, 3)); + FmsAuxiliaryTypeDO type = new FmsAuxiliaryTypeDO().setId(auxiliaryTypeId) + .setName("客户").setAccountSetId(accountSetId) + .setType(FmsAuxiliaryTypeEnum.CUSTOMER.getType()); + FmsAuxiliaryItemDO item = new FmsAuxiliaryItemDO().setId(31L).setCode("C001") + .setName("北京客户").setAuxiliaryTypeId(auxiliaryTypeId) + .setAccountSetId(accountSetId); + when(auxiliaryTypeService.getAuxiliaryTypeList(accountSetId, 10L)) + .thenReturn(Collections.singletonList(type)); + when(auxiliaryTypeService.validateAuxiliaryTypeList(eq(accountSetId), anyCollection())) + .thenReturn(Collections.singletonList(type)); + when(auxiliaryItemService.getAuxiliaryItemListByAccountSetId(accountSetId, 10L)) + .thenReturn(Collections.singletonList(item)); + when(auxiliaryItemService.validateAuxiliaryItemList(eq(accountSetId), anyList())) + .thenReturn(Collections.singletonList(item)); + when(auxiliaryCombinationService.saveAuxiliaryCombination( + eq(accountSetId), eq(subject.getId()), anyList())) + .thenReturn(new FmsAuxiliaryCombinationDO().setId(41L)); + // 准备参数 + FmsInitialBalanceExcelVO row = new FmsInitialBalanceExcelVO().setRowNumber(3) + .setSubjectCode(subject.getCode()).setDirectionName("借") + .setAuxiliaryItems("客户:北京客户").setOpeningAmount(new BigDecimal("500.00")); + + // 调用 + int count = initialBalanceService.importInitialBalance( + accountSetId, Collections.singletonList(row), 10L); + + // 断言 + assertEquals(1, count); + FmsInitialBalanceDO balance = initialBalanceMapper.selectByAccountSetIdAndSubjectId( + accountSetId, subject.getId()); + assertEquals(1, balance.getAssistBalances().size()); + assertEquals(item.getId(), balance.getAssistBalances().get(0) + .getAuxiliaries().get(0).getItemId()); + assertEquals("北京客户", balance.getAssistBalances().get(0) + .getAuxiliaries().get(0).getName()); + assertEquals(new BigDecimal("500.00"), balance.getOpeningAmount()); + } + + @Test + public void testGetInitialBalanceCountByAuxiliaryItemIds_embeddedJson() { + // mock 数据 + initialBalanceMapper.insert(new FmsInitialBalanceDO().setAccountSetId(1L) + .setAssistBalances(Collections.singletonList( + FmsInitialBalanceDO.AssistBalance.builder() + .auxiliaries(Collections.singletonList( + FmsInitialBalanceDO.AuxiliaryItem.builder() + .typeId(31L).itemId(41L).build())) + .build()))); + + // 调用,并断言 + assertEquals(1L, initialBalanceService.getInitialBalanceCountByAuxiliaryItemIds( + 1L, Collections.singletonList(41L))); + assertEquals(1L, initialBalanceService.getInitialBalanceCountByAuxiliaryTypeId(1L, 31L)); + assertEquals(0L, initialBalanceService.getInitialBalanceCountByAuxiliaryItemIds( + 1L, Collections.singletonList(42L))); + } + + @Test + public void testGetInitialBalanceQuantityCountBySubjectIds() { + // mock 数据 + initialBalanceMapper.insert(buildBalance(1L, 101L, "100.00") + .setOpeningQuantity(new BigDecimal("2.00"))); + initialBalanceMapper.insert(buildBalance(1L, 102L, "200.00")); + initialBalanceMapper.insert(buildBalance(1L, 103L, "300.00") + .setAssistBalances(Collections.singletonList(FmsInitialBalanceDO.AssistBalance.builder() + .yearDebitQuantity(new BigDecimal("3.00")).build()))); + + // 调用,并断言 + assertEquals(1L, initialBalanceService.getInitialBalanceQuantityCountBySubjectIds( + 1L, Arrays.asList(101L, 102L))); + assertEquals(2L, initialBalanceService.getInitialBalanceQuantityCountBySubjectIds( + 1L, Arrays.asList(101L, 102L, 103L))); + } + + // ========== 随机对象 ========== + + private FmsSubjectDO buildSubject(Long id, Long parentId, Integer direction) { + return randomPojo(FmsSubjectDO.class, subject -> subject.setId(id) + .setCode(String.valueOf(id)).setName("测试科目") + .setParentId(parentId).setType(FmsSubjectTypeEnum.ASSET.getType()).setCategory(1) + .setBalanceDirection(direction).setQuantityAccounting(false) + .setAuxiliaryTypeIds(Collections.emptyList()).setCurrencyIds(Collections.emptyList())); + } + + private FmsInitialBalanceDO buildBalance(Long accountSetId, Long subjectId, String amount) { + return randomPojo(FmsInitialBalanceDO.class, balance -> balance.setId(null) + .setAccountSetId(accountSetId).setSubjectId(subjectId) + .setAuxiliaryAccounting(false).setOpeningAmount(new BigDecimal(amount)) + .setOpeningQuantity(BigDecimal.ZERO).setYearDebitAmount(BigDecimal.ZERO) + .setYearDebitQuantity(BigDecimal.ZERO).setYearCreditAmount(BigDecimal.ZERO) + .setYearCreditQuantity(BigDecimal.ZERO).setYearOpeningAmount(new BigDecimal(amount)) + .setYearOpeningQuantity(BigDecimal.ZERO).setProfitLossAmount(BigDecimal.ZERO) + .setProfitLossQuantity(BigDecimal.ZERO) + .setAssistBalances(Collections.emptyList())); + } + + private FmsInitialBalanceSaveReqVO.Balance buildSaveBalance(Long subjectId, + String openingAmount, String yearDebitAmount, String yearCreditAmount, + String yearOpeningAmount) { + FmsInitialBalanceSaveReqVO.Balance balance = new FmsInitialBalanceSaveReqVO.Balance(); + balance.setSubjectId(subjectId).setAssistBalances(Collections.emptyList()); + balance.setOpeningAmount(new BigDecimal(openingAmount)).setOpeningQuantity(BigDecimal.ZERO) + .setYearDebitAmount(new BigDecimal(yearDebitAmount)).setYearDebitQuantity(BigDecimal.ZERO) + .setYearCreditAmount(new BigDecimal(yearCreditAmount)).setYearCreditQuantity(BigDecimal.ZERO) + .setYearOpeningAmount(new BigDecimal(yearOpeningAmount)).setYearOpeningQuantity(BigDecimal.ZERO) + .setProfitLossAmount(BigDecimal.ZERO).setProfitLossQuantity(BigDecimal.ZERO); + return balance; + } + + private void mockAccountSetAndSubjects(Long accountSetId, List subjects) { + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(accountSetId) + .setStartTime(LocalDateTime.of(2026, 3, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(accountSetId, 10L)) + .thenReturn(accountSet); + when(accountSetService.validateAccountSetWritePermission(accountSetId, 10L)) + .thenReturn(accountSet); + when(subjectService.getSubjectList(eq(accountSetId), any(), eq(10L))).thenReturn(subjects); + } +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectServiceImplTest.java new file mode 100644 index 0000000..410f0dc --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsSubjectServiceImplTest.java @@ -0,0 +1,823 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectDeleteReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportExcelVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectStatusReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectUsageRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectTemplateDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsSubjectMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsSubjectTemplateMapper; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingTemplateService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryCombinationService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import cn.iocoder.yudao.module.fms.service.config.FmsCurrencyService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsSubjectServiceImpl.class) +public class FmsSubjectServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsSubjectServiceImpl subjectService; + @Resource + private FmsSubjectMapper subjectMapper; + @Resource + private FmsSubjectTemplateMapper subjectTemplateMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsFinanceParameterService financeParameterService; + @MockitoBean + private FmsAuxiliaryTypeService auxiliaryTypeService; + @MockitoBean + private FmsAuxiliaryItemService auxiliaryItemService; + @MockitoBean + private FmsCurrencyService currencyService; + @MockitoBean + private FmsVoucherService voucherService; + @MockitoBean + private FmsVoucherTemplateService voucherTemplateService; + @MockitoBean + private FmsInitialBalanceService initialBalanceService; + @MockitoBean + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + @MockitoBean + private FmsClosingTemplateService closingTemplateService; + + @Test + public void testInitializeDefaultSubjects_success() { + // mock 数据 + when(financeParameterService.getFinanceParameter(10L)).thenReturn(new FmsFinanceParameterDO() + .setSubjectCodeRule(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE)); + when(financeParameterService.convertStandardSubjectCode(anyString(), anyString())) + .thenAnswer(invocation -> invocation.getArgument(0)); + FmsSubjectTemplateDO parent = buildSubjectTemplate( + "1001", "库存现金", FmsSubjectTemplateDO.PARENT_ID_ROOT, 1); + subjectTemplateMapper.insert(parent); + FmsSubjectTemplateDO child = buildSubjectTemplate("100101", "人民币现金", parent.getId(), 2); + subjectTemplateMapper.insert(child); + // 准备参数 + Long accountSetId = 10L; + + // 调用 + subjectService.initializeDefaultSubjects(accountSetId); + + // 断言 + List subjects = subjectMapper.selectListByAccountSetIdAndType( + accountSetId, FmsSubjectTypeEnum.ASSET.getType()); + assertEquals(2, subjects.size()); + assertEquals(FmsSubjectDO.PARENT_ID_ROOT, subjects.get(0).getParentId()); + assertEquals(subjects.get(0).getId(), subjects.get(1).getParentId()); + assertEquals("100101", subjects.get(1).getCode()); + assertTrue(subjects.get(1).getAuxiliaryTypeIds().isEmpty()); + } + + @Test + public void testCreateSubject_success() { + // 准备参数 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "1001", "库存现金"); + + // 调用 + Long subjectId = subjectService.createSubject(reqVO, 10L); + + // 断言 + FmsSubjectDO subject = subjectMapper.selectById(subjectId); + assertEquals(accountSetId, subject.getAccountSetId()); + assertEquals(reqVO.getCode(), subject.getCode()); + assertEquals(reqVO.getName(), subject.getName()); + assertEquals(1, subject.getLevel()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), subject.getStatus()); + assertTrue(subject.getAuxiliaryTypeIds().isEmpty()); + assertTrue(subject.getCurrencyIds().isEmpty()); + } + + @Test + public void testCreateSubject_withCurrency() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsCurrencyDO currency = new FmsCurrencyDO().setId(20L).setAccountSetId(accountSetId) + .setCode("USD").setName("美元").setStandard(false); + when(currencyService.validateCurrencyList(accountSetId, Arrays.asList(currency.getId(), currency.getId()))) + .thenReturn(Collections.singletonList(currency)); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "1001", "库存现金"); + reqVO.setCurrencyIds(Arrays.asList(currency.getId(), currency.getId())); + + // 调用 + Long subjectId = subjectService.createSubject(reqVO, 10L); + + // 断言 + assertEquals(Arrays.asList(currency.getId(), currency.getId()), + subjectMapper.selectById(subjectId).getCurrencyIds()); + } + + @Test + public void testCreateSubject_auxiliaryTypeNotExistsInAccountSet() { + // mock 数据 + Long accountSetId = 1L; + Long auxiliaryTypeId = 20L; + mockAccountSetAccess(accountSetId); + doThrow(exception(AUXILIARY_TYPE_NOT_EXISTS)).when(auxiliaryTypeService) + .validateAuxiliaryTypeList(accountSetId, Collections.singletonList(auxiliaryTypeId)); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "1001", "库存现金") + .setAuxiliaryTypeIds(Collections.singletonList(auxiliaryTypeId)); + + // 调用,并断言 + assertServiceException(() -> subjectService.createSubject(reqVO, 10L), AUXILIARY_TYPE_NOT_EXISTS); + assertNull(subjectMapper.selectByAccountSetIdAndCode(accountSetId, reqVO.getCode())); + } + + @Test + public void testCreateSubject_standardCurrencyInvalid() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsCurrencyDO currency = new FmsCurrencyDO().setId(20L).setAccountSetId(accountSetId) + .setCode("RMB").setName("人民币").setStandard(true); + when(currencyService.validateCurrencyList(accountSetId, Collections.singletonList(currency.getId()))) + .thenReturn(Collections.singletonList(currency)); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "1001", "库存现金"); + reqVO.setCurrencyIds(Collections.singletonList(currency.getId())); + + // 调用,并断言 + assertServiceException(() -> subjectService.createSubject(reqVO, 10L), SUBJECT_CURRENCY_INVALID); + } + + @Test + public void testCreateSubject_codeDuplicate() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + subjectMapper.insert(buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1)); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "1001", "库存现金"); + + // 调用,并断言 + assertServiceException(() -> subjectService.createSubject(reqVO, 10L), SUBJECT_CODE_DUPLICATE); + } + + @Test + public void testCreateSubject_childCodeRuleInvalid() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "10010101", "人民币现金"); + reqVO.setParentId(parent.getId()); + + // 调用,并断言 + assertServiceException(() -> subjectService.createSubject(reqVO, 10L), SUBJECT_CODE_RULE_INVALID); + } + + @Test + public void testCreateSubject_parentCategoryInvalid() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "100101", "人民币现金") + .setParentId(parent.getId()).setCategory(2); + + // 调用,并断言 + assertServiceException(() -> subjectService.createSubject(reqVO, 10L), SUBJECT_PARENT_INVALID); + } + + @Test + public void testCreateSubject_clearParentAuxiliaryTypeIds() { + // mock 数据 + Long accountSetId = 1L; + Long auxiliaryTypeId = 20L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1) + .setAuxiliaryTypeIds(Collections.singletonList(auxiliaryTypeId)); + subjectMapper.insert(parent); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "100101", "人民币现金") + .setParentId(parent.getId()) + .setAuxiliaryTypeIds(Collections.singletonList(auxiliaryTypeId)); + + // 调用 + Long subjectId = subjectService.createSubject(reqVO, 10L); + + // 断言 + assertTrue(subjectMapper.selectById(parent.getId()).getAuxiliaryTypeIds().isEmpty()); + assertEquals(Collections.singletonList(auxiliaryTypeId), + subjectMapper.selectById(subjectId).getAuxiliaryTypeIds()); + } + + @Test + public void testCreateSubject_parentInUse() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(parent.getId()))).thenReturn(1L); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "100101", "人民币现金") + .setParentId(parent.getId()); + + // 调用,并断言 + assertServiceException(() -> subjectService.createSubject(reqVO, 10L), SUBJECT_PARENT_IN_USE); + } + + @Test + public void testCreateSubject_migrateParentDataSuccess() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(parent.getId()))).thenReturn(2L); + when(initialBalanceService.getInitialBalanceCountBySubjectIds( + accountSetId, Collections.singletonList(parent.getId()))).thenReturn(1L); + when(auxiliaryCombinationService.getAuxiliaryCombinationCountBySubjectIds( + accountSetId, Collections.singletonList(parent.getId()))).thenReturn(1L); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "100101", "人民币现金") + .setParentId(parent.getId()).setMigrateParentData(true); + + // 调用 + Long subjectId = subjectService.createSubject(reqVO, 10L); + + // 断言 + FmsSubjectDO subject = subjectMapper.selectById(subjectId); + verify(voucherService).updateVoucherEntrySubject(eq(accountSetId), eq(parent.getId()), + argThat(target -> target.getId().equals(subjectId) + && target.getCode().equals(subject.getCode()) + && target.getParentId().equals(parent.getId()))); + verify(initialBalanceService).updateInitialBalanceSubject(accountSetId, parent.getId(), subjectId); + verify(auxiliaryCombinationService).updateAuxiliaryCombinationSubject( + accountSetId, parent.getId(), subjectId); + } + + @Test + public void testCreateSubject_migrateParentDataConfigIncompatible() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(parent.getId()))).thenReturn(1L); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "100101", "人民币现金") + .setParentId(parent.getId()).setMigrateParentData(true) + .setBalanceDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + + // 调用,并断言 + assertServiceException(() -> subjectService.createSubject(reqVO, 10L), + SUBJECT_PARENT_CONFIG_INCOMPATIBLE); + assertNull(subjectMapper.selectByAccountSetIdAndCode(accountSetId, reqVO.getCode())); + } + + @Test + public void testCreateSubject_migrateParentDataRollback() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(parent.getId()))).thenReturn(1L); + doThrow(new IllegalStateException("迁移失败")).when(initialBalanceService) + .updateInitialBalanceSubject(eq(accountSetId), eq(parent.getId()), anyLong()); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, "100101", "人民币现金") + .setParentId(parent.getId()).setMigrateParentData(true); + + // 调用,并断言 + assertThrows(IllegalStateException.class, () -> subjectService.createSubject(reqVO, 10L)); + assertNull(subjectMapper.selectByAccountSetIdAndCode(accountSetId, reqVO.getCode())); + } + + @Test + public void testUpdateSubject_usedBalanceDirectionImmutable() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(subject); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, subject.getCode(), subject.getName()) + .setId(subject.getId()).setBalanceDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + + // 调用,并断言 + assertServiceException(() -> subjectService.updateSubject(reqVO, 10L), + SUBJECT_USED_BALANCE_DIRECTION_IMMUTABLE); + assertEquals(subject.getBalanceDirection(), subjectMapper.selectById(subject.getId()).getBalanceDirection()); + } + + @Test + public void testUpdateSubject_usedAuxiliaryImmutable() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1) + .setAuxiliaryTypeIds(Collections.singletonList(20L)); + subjectMapper.insert(subject); + when(initialBalanceService.getInitialBalanceCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, subject.getCode(), subject.getName()) + .setId(subject.getId()).setAuxiliaryTypeIds(Collections.singletonList(30L)); + + // 调用,并断言 + assertServiceException(() -> subjectService.updateSubject(reqVO, 10L), + SUBJECT_USED_AUXILIARY_IMMUTABLE); + assertEquals(Collections.singletonList(20L), + subjectMapper.selectById(subject.getId()).getAuxiliaryTypeIds()); + } + + @Test + public void testUpdateSubject_migrateAuxiliarySuccess() { + // mock 数据 + Long accountSetId = 1L; + Long typeId = 20L; + Long itemId = 30L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(subject); + FmsAuxiliaryTypeDO auxiliaryType = new FmsAuxiliaryTypeDO().setId(typeId) + .setAccountSetId(accountSetId).setType(1).setName("客户"); + FmsAuxiliaryItemDO auxiliaryItem = new FmsAuxiliaryItemDO().setId(itemId) + .setAccountSetId(accountSetId).setAuxiliaryTypeId(typeId).setName("测试客户") + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + FmsAuxiliaryCombinationDO combination = new FmsAuxiliaryCombinationDO().setId(40L) + .setAccountSetId(accountSetId).setSubjectId(subject.getId()); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + when(auxiliaryTypeService.validateAuxiliaryTypeList(accountSetId, Collections.singletonList(typeId))) + .thenReturn(Collections.singletonList(auxiliaryType)); + when(auxiliaryItemService.validateAuxiliaryItemList(eq(accountSetId), anyCollection())) + .thenReturn(Collections.singletonList(auxiliaryItem)); + when(auxiliaryCombinationService.saveAuxiliaryCombination( + anyLong(), anyLong(), anyList())).thenReturn(combination); + // 准备参数 + FmsSubjectSaveReqVO.AuxiliaryMapping mapping = new FmsSubjectSaveReqVO.AuxiliaryMapping() + .setTypeId(typeId).setItemId(itemId); + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, subject.getCode(), subject.getName()) + .setId(subject.getId()).setAuxiliaryTypeIds(Collections.singletonList(typeId)) + .setAuxiliaryMappings(Collections.singletonList(mapping)); + + // 调用 + subjectService.updateSubject(reqVO, 10L); + + // 断言 + assertEquals(Collections.singletonList(typeId), + subjectMapper.selectById(subject.getId()).getAuxiliaryTypeIds()); + verify(voucherService).migrateVoucherEntryAuxiliaries(accountSetId, subject.getId(), combination); + } + + @Test + public void testUpdateSubject_migrateAuxiliaryMissingMapping() { + // mock 数据 + Long accountSetId = 1L; + Long typeId = 20L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(subject); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + when(auxiliaryTypeService.validateAuxiliaryTypeList(accountSetId, Collections.singletonList(typeId))) + .thenReturn(Collections.singletonList(new FmsAuxiliaryTypeDO().setId(typeId))); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, subject.getCode(), subject.getName()) + .setId(subject.getId()).setAuxiliaryTypeIds(Collections.singletonList(typeId)); + + // 调用,并断言 + assertServiceException(() -> subjectService.updateSubject(reqVO, 10L), + SUBJECT_AUXILIARY_MIGRATION_INVALID); + assertTrue(subjectMapper.selectById(subject.getId()).getAuxiliaryTypeIds().isEmpty()); + } + + @Test + public void testUpdateSubject_nonLeafConfigImmutable() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + subjectMapper.insert(buildSubjectDO(accountSetId, "100101", parent.getId(), 2)); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, parent.getCode(), parent.getName()) + .setId(parent.getId()).setCategory(2); + + // 调用,并断言 + assertServiceException(() -> subjectService.updateSubject(reqVO, 10L), + SUBJECT_NON_LEAF_CONFIG_IMMUTABLE); + } + + @Test + public void testUpdateSubject_quantityAccountingInUse() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1) + .setQuantityAccounting(true).setQuantityUnit("件"); + subjectMapper.insert(subject); + when(voucherService.getVoucherEntryQuantityCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + // 准备参数 + FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(accountSetId, subject.getCode(), subject.getName()) + .setId(subject.getId()).setQuantityAccounting(false); + + // 调用,并断言 + assertServiceException(() -> subjectService.updateSubject(reqVO, 10L), + SUBJECT_QUANTITY_ACCOUNTING_IN_USE); + } + + @Test + public void testGetSubjectUsage() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(subject); + subjectMapper.insert(buildSubjectDO(accountSetId, "100101", subject.getId(), 2)); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(2L); + when(initialBalanceService.getInitialBalanceCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + when(auxiliaryCombinationService.getAuxiliaryCombinationCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(3L); + when(voucherService.getVoucherEntryQuantityCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + when(initialBalanceService.getInitialBalanceQuantityCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(2L); + + // 调用 + FmsSubjectUsageRespVO usage = subjectService.getSubjectUsage(accountSetId, subject.getId(), 10L); + + // 断言 + assertEquals(1L, usage.getChildCount()); + assertEquals(2L, usage.getVoucherEntryCount()); + assertEquals(1L, usage.getInitialBalanceCount()); + assertEquals(3L, usage.getAuxiliaryCombinationCount()); + assertEquals(3L, usage.getQuantityDataCount()); + assertTrue(usage.getUsed()); + } + + @Test + public void testDeleteSubjectList_hasChildren() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + subjectMapper.insert(buildSubjectDO(accountSetId, "100101", parent.getId(), 2)); + // 准备参数 + FmsSubjectDeleteReqVO deleteReqVO = new FmsSubjectDeleteReqVO() + .setAccountSetId(accountSetId).setIds(Collections.singletonList(parent.getId())); + + // 调用,并断言 + assertServiceException(() -> subjectService.deleteSubjectList(deleteReqVO, 10L), SUBJECT_HAS_CHILDREN); + } + + @Test + public void testDeleteSubjectList_inUse() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(subject); + when(voucherService.getVoucherEntryCountBySubjectIds( + accountSetId, Collections.singletonList(subject.getId()))).thenReturn(1L); + // 准备参数 + FmsSubjectDeleteReqVO deleteReqVO = new FmsSubjectDeleteReqVO() + .setAccountSetId(accountSetId).setIds(Collections.singletonList(subject.getId())); + + // 调用,并断言 + assertServiceException(() -> subjectService.deleteSubjectList(deleteReqVO, 10L), + SUBJECT_VOUCHER_ENTRY_IN_USE, 1L); + assertNotNull(subjectMapper.selectById(subject.getId())); + } + + @Test + public void testDeleteSubjectList_success() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO subject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(subject); + // 准备参数 + FmsSubjectDeleteReqVO deleteReqVO = new FmsSubjectDeleteReqVO() + .setAccountSetId(accountSetId).setIds(Collections.singletonList(subject.getId())); + + // 调用 + subjectService.deleteSubjectList(deleteReqVO, 10L); + + // 断言 + assertNull(subjectMapper.selectById(subject.getId())); + } + + @Test + public void testDeleteSubjectList_recreatedSameCode() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO firstSubject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(firstSubject); + // 准备参数 + FmsSubjectDeleteReqVO firstDeleteReqVO = new FmsSubjectDeleteReqVO() + .setAccountSetId(accountSetId).setIds(Collections.singletonList(firstSubject.getId())); + + // 删除后使用相同编码重新创建 + subjectService.deleteSubjectList(firstDeleteReqVO, 10L); + FmsSubjectDO secondSubject = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(secondSubject); + FmsSubjectDeleteReqVO secondDeleteReqVO = new FmsSubjectDeleteReqVO() + .setAccountSetId(accountSetId).setIds(Collections.singletonList(secondSubject.getId())); + subjectService.deleteSubjectList(secondDeleteReqVO, 10L); + + // 断言 + assertNull(subjectMapper.selectById(firstSubject.getId())); + assertNull(subjectMapper.selectById(secondSubject.getId())); + } + + @Test + public void testUpdateSubjectStatus_success() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO first = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + FmsSubjectDO second = buildSubjectDO(accountSetId, "1002", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(first); + subjectMapper.insert(second); + FmsSubjectDO child = buildSubjectDO(accountSetId, "100101", first.getId(), 2); + subjectMapper.insert(child); + FmsSubjectDO grandchild = buildSubjectDO(accountSetId, "10010101", child.getId(), 3); + subjectMapper.insert(grandchild); + // 准备参数 + FmsSubjectStatusReqVO statusReqVO = new FmsSubjectStatusReqVO().setAccountSetId(accountSetId) + .setIds(Arrays.asList(first.getId(), second.getId())) + .setStatus(CommonStatusEnum.DISABLE.getStatus()); + + // 调用 + subjectService.updateSubjectStatus(statusReqVO, 10L); + + // 断言 + assertEquals(CommonStatusEnum.DISABLE.getStatus(), + subjectMapper.selectById(first.getId()).getStatus()); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), + subjectMapper.selectById(second.getId()).getStatus()); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), + subjectMapper.selectById(child.getId()).getStatus()); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), + subjectMapper.selectById(grandchild.getId()).getStatus()); + } + + @Test + public void testGetSubjectIdListWithChildren_success() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + FmsSubjectDO parent = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(parent); + FmsSubjectDO child = buildSubjectDO(accountSetId, "100101", parent.getId(), 2) + .setStatus(CommonStatusEnum.DISABLE.getStatus()); + subjectMapper.insert(child); + FmsSubjectDO grandchild = buildSubjectDO(accountSetId, "10010101", child.getId(), 3); + subjectMapper.insert(grandchild); + subjectMapper.insert(buildSubjectDO(accountSetId, "1002", FmsSubjectDO.PARENT_ID_ROOT, 1)); + + // 调用 + List subjectIds = subjectService.getSubjectIdListWithChildren(accountSetId, parent.getId()); + + // 断言 + assertEquals(Arrays.asList(parent.getId(), child.getId(), grandchild.getId()), subjectIds); + } + + @Test + public void testGetSubjectCountByAuxiliaryTypeId() { + // mock 数据 + subjectMapper.insert(buildSubjectDO(1L, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1) + .setAuxiliaryTypeIds(Collections.singletonList(20L))); + subjectMapper.insert(buildSubjectDO(2L, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1) + .setAuxiliaryTypeIds(Collections.singletonList(30L))); + + // 调用,并断言 + assertEquals(1L, subjectService.getSubjectCountByAuxiliaryTypeId(1L, 20L)); + assertEquals(0L, subjectService.getSubjectCountByAuxiliaryTypeId(1L, 30L)); + assertEquals(0L, subjectService.getSubjectCountByAuxiliaryTypeId(2L, 20L)); + } + + @Test + public void testGetSubjectCountByCurrencyId() { + // mock 数据 + subjectMapper.insert(buildSubjectDO(1L, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1) + .setCurrencyIds(Collections.singletonList(20L))); + subjectMapper.insert(buildSubjectDO(2L, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1) + .setCurrencyIds(Collections.singletonList(30L))); + + // 调用,并断言 + assertEquals(1L, subjectService.getSubjectCountByCurrencyId(1L, 20L)); + assertEquals(0L, subjectService.getSubjectCountByCurrencyId(1L, 30L)); + assertEquals(0L, subjectService.getSubjectCountByCurrencyId(2L, 20L)); + } + + @Test + public void testExpandSubjectCodes() { + // mock 数据 + Long accountSetId = 1L; + FmsSubjectDO levelOne = buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1); + subjectMapper.insert(levelOne); + FmsSubjectDO levelTwo = buildSubjectDO(accountSetId, "100101", levelOne.getId(), 2); + subjectMapper.insert(levelTwo); + FmsSubjectDO levelThree = buildSubjectDO(accountSetId, "10010101", levelTwo.getId(), 3); + subjectMapper.insert(levelThree); + + // 调用 + subjectService.expandSubjectCodes(accountSetId, + Arrays.asList(4, 2, 2, 2), Arrays.asList(5, 3, 2, 2)); + + // 断言 + assertEquals("10001", subjectMapper.selectById(levelOne.getId()).getCode()); + assertEquals("10001001", subjectMapper.selectById(levelTwo.getId()).getCode()); + assertEquals("1000100101", subjectMapper.selectById(levelThree.getId()).getCode()); + } + + @Test + public void testImportSubjectList_success() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + // 准备参数 + List importSubjects = Arrays.asList( + FmsSubjectImportExcelVO.builder().code("100101").name("人民币现金") + .parentSubjectCode("1001").balanceDirection("借").categoryName("流动资产").build(), + FmsSubjectImportExcelVO.builder().code("1001").name("库存现金") + .parentSubjectCode("0").balanceDirection("借").categoryName("流动资产").build()); + + // 调用 + FmsSubjectImportRespVO result = subjectService.importSubjectList(accountSetId, importSubjects, 10L); + + // 断言 + assertEquals(2, result.getTotalCount()); + assertEquals(Arrays.asList("1001", "100101"), result.getSuccessSubjectCodes()); + assertTrue(result.getFailureReasons().isEmpty()); + FmsSubjectDO parent = subjectMapper.selectByAccountSetIdAndCode(accountSetId, "1001"); + FmsSubjectDO child = subjectMapper.selectByAccountSetIdAndCode(accountSetId, "100101"); + assertEquals(parent.getId(), child.getParentId()); + assertEquals(2, child.getLevel()); + } + + @Test + public void testImportSubjectList_empty() { + // 准备参数 + Long accountSetId = 1L; + + // 调用,并断言 + assertServiceException(() -> subjectService.importSubjectList( + accountSetId, Collections.emptyList(), 10L), SUBJECT_IMPORT_LIST_IS_EMPTY); + } + + @Test + public void testImportSubjectList_requiredFieldInvalid() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + // 准备参数 + FmsSubjectImportExcelVO importSubject = FmsSubjectImportExcelVO.builder() + .code(" ").name("测试科目").parentSubjectCode("0") + .balanceDirection("借").categoryName("流动资产").build(); + + // 调用 + FmsSubjectImportRespVO result = subjectService.importSubjectList( + accountSetId, Collections.singletonList(importSubject), 10L); + + // 断言 + assertTrue(result.getSuccessSubjectCodes().isEmpty()); + assertEquals(1, result.getFailureReasons().size()); + assertTrue(CollUtil.getFirst(result.getFailureReasons().values()).contains("科目编码不能为空")); + } + + @Test + public void testImportSubjectList_partialFailure() { + // mock 数据 + Long accountSetId = 1L; + mockAccountSetAccess(accountSetId); + subjectMapper.insert(buildSubjectDO(accountSetId, "1001", FmsSubjectDO.PARENT_ID_ROOT, 1)); + // 准备参数 + List importSubjects = Arrays.asList( + FmsSubjectImportExcelVO.builder().code("1001").name("重复科目") + .parentSubjectCode("0").balanceDirection("借").categoryName("流动资产").build(), + FmsSubjectImportExcelVO.builder().code("1002").name("银行存款") + .parentSubjectCode("0").balanceDirection("错误方向").categoryName("流动资产").build(), + FmsSubjectImportExcelVO.builder().code("1003").name("其他货币资金") + .parentSubjectCode("0").balanceDirection("借").categoryName("流动资产").build()); + + // 调用 + FmsSubjectImportRespVO result = subjectService.importSubjectList(accountSetId, importSubjects, 10L); + + // 断言 + assertEquals(Arrays.asList("1001", "1003"), result.getSuccessSubjectCodes()); + assertEquals(1, result.getFailureReasons().size()); + assertTrue(result.getFailureReasons().values().contains("余额方向只能填写借或贷")); + assertEquals("重复科目", subjectMapper.selectByAccountSetIdAndCode(accountSetId, "1001").getName()); + assertNotNull(subjectMapper.selectByAccountSetIdAndCode(accountSetId, "1003")); + } + + // ========== 随机对象 ========== + + private FmsSubjectSaveReqVO buildSubjectSaveReqVO(Long accountSetId, String subjectCode, String name) { + FmsSubjectSaveReqVO reqVO = new FmsSubjectSaveReqVO(); + reqVO.setAccountSetId(accountSetId).setCode(subjectCode).setName(name) + .setParentId(FmsSubjectDO.PARENT_ID_ROOT) + .setType(FmsSubjectTypeEnum.ASSET.getType()).setCategory(1) + .setBalanceDirection(FmsDebitCreditDirectionEnum.DEBIT.getType()) + .setAuxiliaryTypeIds(Collections.emptyList()).setCurrencyIds(Collections.emptyList()) + .setQuantityAccounting(false).setCash(false); + return reqVO; + } + + private FmsSubjectDO buildSubjectDO(Long accountSetId, String subjectCode, Long parentId, int level) { + return randomPojo(FmsSubjectDO.class, subject -> subject.setId(null) + .setCode(subjectCode).setName("测试科目").setParentId(parentId) + .setType(FmsSubjectTypeEnum.ASSET.getType()).setCategory(1) + .setBalanceDirection(FmsDebitCreditDirectionEnum.DEBIT.getType()) + .setCash(false).setStatus(CommonStatusEnum.ENABLE.getStatus()).setLevel(level) + .setQuantityAccounting(false).setAccountSetId(accountSetId) + .setAuxiliaryTypeIds(Collections.emptyList()).setCurrencyIds(Collections.emptyList())); + } + + private FmsSubjectTemplateDO buildSubjectTemplate( + String subjectCode, String name, Long parentId, int level) { + return randomPojo(FmsSubjectTemplateDO.class, subject -> subject.setId(null) + .setCode(subjectCode).setName(name).setParentId(parentId) + .setType(FmsSubjectTypeEnum.ASSET.getType()).setCategory(1) + .setBalanceDirection(FmsDebitCreditDirectionEnum.DEBIT.getType()) + .setCash(true).setStatus(CommonStatusEnum.ENABLE.getStatus()).setLevel(level) + .setQuantityAccounting(false)); + } + + private void mockAccountSetAccess(Long accountSetId) { + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(accountSetId); + when(accountSetService.validateAccountSetReadPermission(accountSetId, 10L)) + .thenReturn(accountSet); + when(accountSetService.validateAccountSetWritePermission(accountSetId, 10L)) + .thenReturn(accountSet); + when(financeParameterService.getFinanceParameter(accountSetId)) + .thenReturn(new FmsFinanceParameterDO() + .setLevel(FmsFinanceParameterDO.DEFAULT_LEVEL) + .setSubjectCodeRule(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateServiceImplTest.java new file mode 100644 index 0000000..5e0b31d --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherTemplateServiceImplTest.java @@ -0,0 +1,356 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory.FmsVoucherTemplateCategorySaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateEntryVO; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsVoucherTemplateCategoryMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsVoucherTemplateMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import(FmsVoucherTemplateServiceImpl.class) +public class FmsVoucherTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsVoucherTemplateServiceImpl voucherTemplateService; + @Resource + private FmsVoucherTemplateCategoryMapper templateCategoryMapper; + @Resource + private FmsVoucherTemplateMapper templateMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsAuxiliaryItemService auxiliaryItemService; + @MockitoBean + private FmsAuxiliaryTypeService auxiliaryTypeService; + + @Test + public void testCreateTemplateCategory_success() { + // 准备参数 + FmsVoucherTemplateCategorySaveReqVO reqVO = buildCategorySaveReqVO(null, 1L, "日常收支"); + + // 调用 + Long categoryId = voucherTemplateService.createTemplateCategory(reqVO, 10L); + + // 断言 + FmsVoucherTemplateCategoryDO category = templateCategoryMapper.selectById(categoryId); + assertEquals("日常收支", category.getName()); + assertEquals(1L, category.getAccountSetId()); + } + + @Test + public void testCreateTemplateCategory_nameDuplicate() { + // mock 数据 + templateCategoryMapper.insert(buildCategory(1L, "日常收支")); + // 准备参数 + FmsVoucherTemplateCategorySaveReqVO reqVO = buildCategorySaveReqVO(null, 1L, "日常收支"); + + // 调用,并断言 + assertServiceException(() -> voucherTemplateService.createTemplateCategory(reqVO, 10L), + VOUCHER_TEMPLATE_CATEGORY_NAME_DUPLICATE); + } + + @Test + public void testDeleteTemplateCategory_inUse() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + templateMapper.insert(new FmsVoucherTemplateDO().setAccountSetId(1L) + .setCategoryId(category.getId()).setName("办公用品采购") + .setEntries(Collections.emptyList())); + + // 调用,并断言 + assertServiceException(() -> voucherTemplateService.deleteTemplateCategory( + 1L, category.getId(), 10L), VOUCHER_TEMPLATE_CATEGORY_IN_USE); + } + + @Test + public void testCreateVoucherTemplate_jsonContent() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.singletonList(31L)), + new FmsSubjectDO().setId(201L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()))); + when(auxiliaryItemService.validateAuxiliaryItemList(eq(1L), anyCollection())) + .thenReturn(Collections.singletonList(new FmsAuxiliaryItemDO().setId(41L) + .setAuxiliaryTypeId(31L).setName("上海客户") + .setStatus(CommonStatusEnum.ENABLE.getStatus()))); + when(auxiliaryTypeService.validateAuxiliaryTypeList(eq(1L), anyCollection())) + .thenReturn(Collections.singletonList(new FmsAuxiliaryTypeDO().setId(31L).setType(1))); + // 准备参数 + FmsVoucherTemplateSaveReqVO reqVO = buildTemplateSaveReqVO(category.getId()); + FmsVoucherTemplateEntryVO.AuxiliaryItem auxiliary = new FmsVoucherTemplateEntryVO.AuxiliaryItem(); + auxiliary.setTypeId(31L); + auxiliary.setItemId(41L); + CollUtil.getFirst(reqVO.getEntries()).setAuxiliaries(Collections.singletonList(auxiliary)); + + // 调用 + Long templateId = voucherTemplateService.createVoucherTemplate(reqVO, 10L); + + // 断言 + FmsVoucherTemplateDO template = templateMapper.selectById(templateId); + assertEquals("办公用品采购", template.getName()); + assertEquals(2, template.getEntries().size()); + assertEquals(new BigDecimal("100.00"), CollUtil.getFirst(template.getEntries()).getDebitAmount()); + FmsVoucherEntryDO.AuxiliaryItem auxiliaryItem = CollUtil.getFirst( + CollUtil.getFirst(template.getEntries()).getAuxiliaries()); + assertEquals(1, auxiliaryItem.getType()); + assertEquals(31L, auxiliaryItem.getTypeId()); + assertEquals(41L, auxiliaryItem.getItemId()); + assertEquals("上海客户", auxiliaryItem.getName()); + } + + @Test + public void testCreateVoucherTemplate_negativeAmount() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()), + new FmsSubjectDO().setId(201L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()))); + // 准备参数 + FmsVoucherTemplateSaveReqVO reqVO = buildTemplateSaveReqVO(category.getId()); + reqVO.getEntries().get(0).setDebitAmount(new BigDecimal("-20.00")); + reqVO.getEntries().get(1).setCreditAmount(new BigDecimal("-20.00")); + + // 调用 + Long templateId = voucherTemplateService.createVoucherTemplate(reqVO, 10L); + + // 断言 + FmsVoucherTemplateDO template = templateMapper.selectById(templateId); + assertEquals(new BigDecimal("-20.00"), CollUtil.getFirst(template.getEntries()).getDebitAmount()); + } + + @Test + public void testCreateVoucherTemplate_amountUnbalanced() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()), + new FmsSubjectDO().setId(201L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()))); + // 准备参数 + FmsVoucherTemplateSaveReqVO reqVO = buildTemplateSaveReqVO(category.getId()); + reqVO.getEntries().get(1).setCreditAmount(new BigDecimal("90.00")); + + // 调用,并断言 + assertServiceException(() -> voucherTemplateService.createVoucherTemplate(reqVO, 10L), + VOUCHER_AMOUNT_UNBALANCED); + } + + @Test + public void testCreateVoucherTemplate_quantityAmountFloor() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setQuantityAccounting(true).setAuxiliaryTypeIds(Collections.emptyList()), + new FmsSubjectDO().setId(201L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()))); + // 准备参数 + FmsVoucherTemplateSaveReqVO reqVO = buildTemplateSaveReqVO(category.getId()); + CollUtil.getFirst(reqVO.getEntries()).setDebitAmount(new BigDecimal("1.99")); + CollUtil.getFirst(reqVO.getEntries()).setQuantity(new BigDecimal("1")); + CollUtil.getFirst(reqVO.getEntries()).setUnitPrice(new BigDecimal("1.999")); + reqVO.getEntries().get(1).setCreditAmount(new BigDecimal("1.99")); + + // 调用 + Long templateId = voucherTemplateService.createVoucherTemplate(reqVO, 10L); + + // 断言 + FmsVoucherTemplateDO template = templateMapper.selectById(templateId); + assertEquals(new BigDecimal("1.99"), CollUtil.getFirst(template.getEntries()).getDebitAmount()); + } + + @Test + public void testCreateVoucherTemplate_subjectDisabled() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setStatus(CommonStatusEnum.DISABLE.getStatus()), + new FmsSubjectDO().setId(201L).setStatus(CommonStatusEnum.ENABLE.getStatus()))); + // 准备参数 + FmsVoucherTemplateSaveReqVO reqVO = buildTemplateSaveReqVO(category.getId()); + + // 调用,并断言 + assertServiceException(() -> voucherTemplateService.createVoucherTemplate(reqVO, 10L), + VOUCHER_SUBJECT_DISABLED); + } + + @Test + public void testCreateVoucherTemplate_auxiliaryItemDisabled() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.singletonList(31L)), + new FmsSubjectDO().setId(201L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()))); + when(auxiliaryItemService.validateAuxiliaryItemList(eq(1L), anyCollection())) + .thenReturn(Collections.singletonList(new FmsAuxiliaryItemDO().setId(41L) + .setAuxiliaryTypeId(31L).setName("上海客户") + .setStatus(CommonStatusEnum.DISABLE.getStatus()))); + when(auxiliaryTypeService.validateAuxiliaryTypeList(eq(1L), anyCollection())) + .thenReturn(Collections.singletonList(new FmsAuxiliaryTypeDO().setId(31L).setType(1))); + // 准备参数 + FmsVoucherTemplateSaveReqVO reqVO = buildTemplateSaveReqVO(category.getId()); + FmsVoucherTemplateEntryVO.AuxiliaryItem auxiliary = new FmsVoucherTemplateEntryVO.AuxiliaryItem(); + auxiliary.setTypeId(31L); + auxiliary.setItemId(41L); + CollUtil.getFirst(reqVO.getEntries()).setAuxiliaries(Collections.singletonList(auxiliary)); + + // 调用,并断言 + assertServiceException(() -> voucherTemplateService.createVoucherTemplate(reqVO, 10L), + VOUCHER_AUXILIARY_REQUIRED); + } + + @Test + public void testUpdateVoucherTemplate_success() { + // mock 数据 + FmsVoucherTemplateCategoryDO category = buildCategory(1L, "日常收支"); + templateCategoryMapper.insert(category); + FmsVoucherTemplateDO template = new FmsVoucherTemplateDO().setAccountSetId(1L) + .setCategoryId(category.getId()).setName("办公用品采购") + .setEntries(Collections.emptyList()); + templateMapper.insert(template); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(Arrays.asList( + new FmsSubjectDO().setId(101L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()), + new FmsSubjectDO().setId(201L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setAuxiliaryTypeIds(Collections.emptyList()))); + // 准备参数 + FmsVoucherTemplateSaveReqVO reqVO = buildTemplateSaveReqVO(category.getId()); + reqVO.setId(template.getId()); + reqVO.setName("办公用品采购(已修改)"); + + // 调用 + voucherTemplateService.updateVoucherTemplate(reqVO, 10L); + + // 断言 + FmsVoucherTemplateDO dbTemplate = templateMapper.selectById(template.getId()); + assertEquals("办公用品采购(已修改)", dbTemplate.getName()); + assertEquals(category.getId(), dbTemplate.getCategoryId()); + assertEquals(2, dbTemplate.getEntries().size()); + } + + @Test + public void testDeleteVoucherTemplate_success() { + // mock 数据 + FmsVoucherTemplateDO template = new FmsVoucherTemplateDO().setAccountSetId(1L) + .setCategoryId(11L).setName("办公用品采购").setEntries(Collections.emptyList()); + templateMapper.insert(template); + + // 调用 + voucherTemplateService.deleteVoucherTemplate(1L, template.getId(), 10L); + + // 断言 + assertNull(templateMapper.selectById(template.getId())); + } + + @Test + public void testDeleteVoucherTemplate_wrongAccountSet() { + // mock 数据 + FmsVoucherTemplateDO template = new FmsVoucherTemplateDO().setAccountSetId(1L) + .setCategoryId(11L).setName("办公用品采购").setEntries(Collections.emptyList()); + templateMapper.insert(template); + + // 调用,并断言 + assertServiceException(() -> voucherTemplateService.deleteVoucherTemplate( + 2L, template.getId(), 10L), VOUCHER_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testGetVoucherTemplateCountByAuxiliary() { + // mock 数据 + FmsVoucherTemplateDO.Entry entry = new FmsVoucherTemplateDO.Entry().setAuxiliaries( + Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .typeId(31L).itemId(41L).build())); + templateMapper.insert(new FmsVoucherTemplateDO().setName("收款模板") + .setCategoryId(1L).setAccountSetId(1L) + .setEntries(Collections.singletonList(entry))); + templateMapper.insert(new FmsVoucherTemplateDO().setName("其他账套模板") + .setCategoryId(1L).setAccountSetId(2L) + .setEntries(Collections.singletonList(entry))); + + // 调用,并断言 + assertEquals(1L, voucherTemplateService.getVoucherTemplateCountByAuxiliaryItemIds( + 1L, Collections.singletonList(41L))); + assertEquals(1L, voucherTemplateService.getVoucherTemplateCountByAuxiliaryTypeId(1L, 31L)); + assertEquals(0L, voucherTemplateService.getVoucherTemplateCountByAuxiliaryItemIds( + 1L, Collections.singletonList(42L))); + assertEquals(0L, voucherTemplateService.getVoucherTemplateCountByAuxiliaryItemIds( + 1L, Collections.emptyList())); + } + + // ========== 随机对象 ========== + + private FmsVoucherTemplateCategorySaveReqVO buildCategorySaveReqVO( + Long id, Long accountSetId, String name) { + FmsVoucherTemplateCategorySaveReqVO reqVO = new FmsVoucherTemplateCategorySaveReqVO(); + reqVO.setId(id); + reqVO.setAccountSetId(accountSetId); + reqVO.setName(name); + return reqVO; + } + + private FmsVoucherTemplateCategoryDO buildCategory(Long accountSetId, String name) { + return randomPojo(FmsVoucherTemplateCategoryDO.class, category -> category.setId(null) + .setAccountSetId(accountSetId).setName(name)); + } + + private FmsVoucherTemplateSaveReqVO buildTemplateSaveReqVO(Long categoryId) { + FmsVoucherTemplateEntryVO debitEntry = new FmsVoucherTemplateEntryVO(); + debitEntry.setDigest("购买办公用品"); + debitEntry.setSubjectId(101L); + debitEntry.setDebitAmount(new BigDecimal("100.00")); + debitEntry.setAuxiliaries(Collections.emptyList()); + FmsVoucherTemplateEntryVO creditEntry = new FmsVoucherTemplateEntryVO(); + creditEntry.setDigest("购买办公用品"); + creditEntry.setSubjectId(201L); + creditEntry.setCreditAmount(new BigDecimal("100.00")); + creditEntry.setAuxiliaries(Collections.emptyList()); + FmsVoucherTemplateSaveReqVO reqVO = new FmsVoucherTemplateSaveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setCategoryId(categoryId); + reqVO.setName("办公用品采购"); + reqVO.setEntries(Arrays.asList(debitEntry, creditEntry)); + return reqVO; + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordServiceImplTest.java new file mode 100644 index 0000000..44baf3a --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/config/FmsVoucherWordServiceImplTest.java @@ -0,0 +1,224 @@ +package cn.iocoder.yudao.module.fms.service.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword.FmsVoucherWordSaveReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsVoucherWordMapper; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@Import(FmsVoucherWordServiceImpl.class) +public class FmsVoucherWordServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsVoucherWordServiceImpl voucherWordService; + @Resource + private FmsVoucherWordMapper voucherWordMapper; + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsVoucherService voucherService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + + @Test + public void testCreateVoucherWord_success() { + // mock 数据 + voucherWordMapper.insert(buildVoucherWord(1L, "记", true, 1)); + // 准备参数 + FmsVoucherWordSaveReqVO reqVO = buildSaveReqVO(null, 1L, "转", "转账凭证", false); + + // 调用 + Long voucherWordId = voucherWordService.createVoucherWord(reqVO, 10L); + + // 断言 + FmsVoucherWordDO voucherWord = voucherWordMapper.selectById(voucherWordId); + assertEquals("转", voucherWord.getName()); + assertEquals("转账凭证", voucherWord.getPrintTitle()); + assertFalse(voucherWord.getDefaultStatus()); + assertEquals(2, voucherWord.getSort()); + } + + @Test + public void testCreateVoucherWord_firstWordDefault() { + // 准备参数 + FmsVoucherWordSaveReqVO reqVO = buildSaveReqVO(null, 1L, "记", "记账凭证", false); + + // 调用 + Long voucherWordId = voucherWordService.createVoucherWord(reqVO, 10L); + + // 断言 + assertTrue(voucherWordMapper.selectById(voucherWordId).getDefaultStatus()); + } + + @Test + public void testCreateVoucherWord_nameDuplicate() { + // mock 数据 + voucherWordMapper.insert(buildVoucherWord(1L, "记", true, 1)); + // 准备参数 + FmsVoucherWordSaveReqVO reqVO = buildSaveReqVO(null, 1L, "记", "记账凭证", false); + + // 调用,并断言 + assertServiceException(() -> voucherWordService.createVoucherWord(reqVO, 10L), + VOUCHER_WORD_NAME_DUPLICATE); + } + + @Test + public void testUpdateVoucherWord_switchDefault() { + // mock 数据 + FmsVoucherWordDO first = buildVoucherWord(1L, "记", true, 1); + voucherWordMapper.insert(first); + FmsVoucherWordDO second = buildVoucherWord(1L, "收", false, 2); + voucherWordMapper.insert(second); + // 准备参数 + FmsVoucherWordSaveReqVO reqVO = buildSaveReqVO( + second.getId(), 1L, "收", "收款凭证", true); + + // 调用 + voucherWordService.updateVoucherWord(reqVO, 10L); + + // 断言 + assertFalse(voucherWordMapper.selectById(first.getId()).getDefaultStatus()); + assertTrue(voucherWordMapper.selectById(second.getId()).getDefaultStatus()); + } + + @Test + public void testUpdateVoucherWord_defaultRequired() { + // mock 数据 + FmsVoucherWordDO voucherWord = buildVoucherWord(1L, "记", true, 1); + voucherWordMapper.insert(voucherWord); + // 准备参数 + FmsVoucherWordSaveReqVO reqVO = buildSaveReqVO( + voucherWord.getId(), 1L, "记", "记账凭证", false); + + // 调用,并断言 + assertServiceException(() -> voucherWordService.updateVoucherWord(reqVO, 10L), + VOUCHER_WORD_DEFAULT_REQUIRED); + } + + @Test + public void testUpdateVoucherWord_inUse() { + // mock 数据 + FmsVoucherWordDO voucherWord = buildVoucherWord(1L, "记", true, 1); + voucherWordMapper.insert(voucherWord); + when(voucherService.getVoucherCountByVoucherWordId(1L, voucherWord.getId())).thenReturn(1L); + // 准备参数 + FmsVoucherWordSaveReqVO reqVO = buildSaveReqVO( + voucherWord.getId(), 1L, "记", "通用记账凭证", true); + + // 调用,并断言 + assertServiceException(() -> voucherWordService.updateVoucherWord(reqVO, 10L), + VOUCHER_WORD_IN_USE_NOT_EDITABLE); + } + + @Test + public void testUpdateVoucherWord_inUseSwitchDefault() { + // mock 数据 + FmsVoucherWordDO first = buildVoucherWord(1L, "记", false, 1); + voucherWordMapper.insert(first); + FmsVoucherWordDO second = buildVoucherWord(1L, "收", true, 2); + voucherWordMapper.insert(second); + when(voucherService.getVoucherCountByVoucherWordId(1L, first.getId())).thenReturn(1L); + // 准备参数 + FmsVoucherWordSaveReqVO reqVO = buildSaveReqVO( + first.getId(), 1L, first.getName(), first.getPrintTitle(), true); + + // 调用 + voucherWordService.updateVoucherWord(reqVO, 10L); + + // 断言 + assertTrue(voucherWordMapper.selectById(first.getId()).getDefaultStatus()); + assertFalse(voucherWordMapper.selectById(second.getId()).getDefaultStatus()); + } + + @Test + public void testDeleteVoucherWord_defaultStatus() { + // mock 数据 + FmsVoucherWordDO voucherWord = buildVoucherWord(1L, "记", true, 1); + voucherWordMapper.insert(voucherWord); + + // 调用,并断言 + assertServiceException(() -> voucherWordService.deleteVoucherWord(1L, voucherWord.getId(), 10L), + VOUCHER_WORD_DEFAULT_NOT_DELETABLE); + } + + @Test + public void testDeleteVoucherWord_inUse() { + // mock 数据 + FmsVoucherWordDO voucherWord = buildVoucherWord(1L, "转", false, 2); + voucherWordMapper.insert(voucherWord); + when(voucherService.getVoucherCountByVoucherWordId(1L, voucherWord.getId())).thenReturn(1L); + + // 调用,并断言 + assertServiceException(() -> voucherWordService.deleteVoucherWord(1L, voucherWord.getId(), 10L), + VOUCHER_WORD_IN_USE_NOT_DELETABLE, 1L); + } + + @Test + public void testDeleteVoucherWord_closingInUse() { + // mock 数据 + FmsVoucherWordDO voucherWord = buildVoucherWord(1L, "转", false, 2); + voucherWordMapper.insert(voucherWord); + // mock 方法 + when(closingSchemeService.getClosingSchemeCountByVoucherWordId(1L, voucherWord.getId())).thenReturn(1L); + + // 调用,并断言 + assertServiceException(() -> voucherWordService.deleteVoucherWord(1L, voucherWord.getId(), 10L), + VOUCHER_WORD_IN_USE_BY_CLOSING_SCHEME_NOT_DELETABLE, 1L); + } + + @Test + public void testInitializeDefaultVoucherWords_success() { + // 准备参数 + Long accountSetId = randomLongId(); + + // 调用 + voucherWordService.initializeDefaultVoucherWords(accountSetId); + + // 断言 + List voucherWords = voucherWordMapper.selectListByAccountSetId(accountSetId); + assertEquals(4, voucherWords.size()); + assertEquals("记", voucherWords.get(0).getName()); + assertEquals("记账凭证", voucherWords.get(0).getPrintTitle()); + assertTrue(voucherWords.get(0).getDefaultStatus()); + assertEquals("收", voucherWords.get(1).getName()); + assertEquals("收款凭证", voucherWords.get(1).getPrintTitle()); + assertFalse(voucherWords.get(1).getDefaultStatus()); + assertEquals("转", voucherWords.get(2).getName()); + assertEquals("转账凭证", voucherWords.get(2).getPrintTitle()); + assertFalse(voucherWords.get(2).getDefaultStatus()); + assertEquals("付", voucherWords.get(3).getName()); + assertEquals("付款凭证", voucherWords.get(3).getPrintTitle()); + assertFalse(voucherWords.get(3).getDefaultStatus()); + } + + // ========== 随机对象 ========== + + private FmsVoucherWordSaveReqVO buildSaveReqVO(Long id, Long accountSetId, String name, + String printTitle, boolean defaultStatus) { + return randomPojo(FmsVoucherWordSaveReqVO.class, reqVO -> reqVO.setId(id) + .setAccountSetId(accountSetId).setName(name).setPrintTitle(printTitle) + .setDefaultStatus(defaultStatus)); + } + + private FmsVoucherWordDO buildVoucherWord( + Long accountSetId, String name, boolean defaultStatus, int sort) { + return randomPojo(FmsVoucherWordDO.class, voucherWord -> voucherWord.setId(null) + .setAccountSetId(accountSetId).setName(name).setPrintTitle(name + "账凭证") + .setDefaultStatus(defaultStatus).setSort(sort)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeServiceImplTest.java new file mode 100644 index 0000000..68bfd3a --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/home/FmsHomeServiceImplTest.java @@ -0,0 +1,192 @@ +package cn.iocoder.yudao.module.fms.service.home; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeMetricDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.home.vo.FmsHomeRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.report.FmsIncomeStatementService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_NOT_INITIALIZED; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.HOME_METRIC_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(FmsHomeServiceImpl.class) +public class FmsHomeServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsHomeServiceImpl homeService; + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsClosingPeriodService closingPeriodService; + @MockitoBean + private FmsIncomeStatementService incomeStatementService; + + @Test + public void testGetHome_success() { + // mock 数据 + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L).setInitialized(true) + .setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(accountSet); + when(closingPeriodService.getCurrentMonth(1L, accountSet.getStartTime())) + .thenReturn(YearMonth.of(2026, 8)); + when(incomeStatementService.getIncomeStatement(any(FmsReportListReqVO.class), eq(10L))) + .thenAnswer(invocation -> buildIncomeStatement( + invocation.getArgument(0).getStartMonth())); + + // 调用 + FmsHomeRespVO result = homeService.getHome(1L, 10L); + + // 断言 + assertEquals("2026-08", result.getCurrentMonth()); + assertEquals(12, result.getTrends().size()); + assertEquals(BigDecimal.ZERO, result.getTrends().get(0).getIncome()); + FmsHomeRespVO.Trend currentTrend = result.getTrends().get(11); + assertEquals(new BigDecimal("100.00"), currentTrend.getIncome()); + assertEquals(new BigDecimal("45.00"), currentTrend.getOperatingCost()); + assertEquals(new BigDecimal("30.00"), currentTrend.getProfit()); + assertEquals(new BigDecimal("12.00"), currentTrend.getExpense()); + assertEquals(new BigDecimal("-13.00"), currentTrend.getOther()); + assertEquals(5, result.getMetrics().size()); + verify(incomeStatementService, times(8)).getIncomeStatement(any(FmsReportListReqVO.class), eq(10L)); + } + + @Test + public void testGetMetricDetail_success() { + // mock 数据 + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L).setInitialized(true) + .setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(accountSet); + when(closingPeriodService.getCurrentMonth(1L, accountSet.getStartTime())) + .thenReturn(YearMonth.of(2026, 8)); + when(incomeStatementService.getIncomeStatement(any(FmsReportListReqVO.class), eq(10L))) + .thenAnswer(invocation -> buildMetricIncomeStatement( + invocation.getArgument(0).getStartMonth())); + + // 调用 + FmsHomeMetricDetailRespVO result = homeService.getMetricDetail(1L, "income", 10L); + + // 断言 + assertEquals("income", result.getKey()); + assertEquals("收入", result.getName()); + assertEquals(12, result.getTrends().size()); + assertEquals(new BigDecimal("100.00"), result.getTrends().get(11).getAmount()); + assertEquals(2, result.getStructure().size()); + assertEquals("5001", result.getStructure().get(0).getSubjectCode()); + assertEquals(new BigDecimal("80.00"), result.getStructure().get(0).getAmount()); + assertEquals("5051", result.getStructure().get(1).getSubjectCode()); + assertEquals(new BigDecimal("20.00"), result.getStructure().get(1).getAmount()); + verify(incomeStatementService, times(8)).getIncomeStatement(any(FmsReportListReqVO.class), eq(10L)); + } + + @Test + public void testGetMetricDetail_metricInvalid() { + // mock 数据 + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setInitialized(true) + .setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0))); + + // 调用,并断言异常 + assertServiceException(() -> homeService.getMetricDetail(1L, "unknown", 10L), + HOME_METRIC_INVALID); + verify(accountSetService).validateAccountSetReadPermission(1L, 10L); + } + + @Test + public void testGetHome_notInitialized() { + // mock 数据 + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setInitialized(false)); + + // 调用,并断言异常 + assertServiceException(() -> homeService.getHome(1L, 10L), ACCOUNT_SET_NOT_INITIALIZED); + } + + @Test + public void testGetMetricDetail_lineFormula() { + // mock 数据 + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L).setInitialized(true) + .setStartTime(LocalDateTime.of(2026, 8, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn(accountSet); + when(closingPeriodService.getCurrentMonth(1L, accountSet.getStartTime())) + .thenReturn(YearMonth.of(2026, 8)); + when(incomeStatementService.getIncomeStatement(any(FmsReportListReqVO.class), eq(10L))) + .thenReturn(Arrays.asList( + new FmsReportItemRespVO().setRowNo(1).setCurrentAmount(new BigDecimal("100.00")) + .setFormula("[{\"subjectId\":1444,\"subjectNumber\":\"5001\"," + + "\"subjectName\":\"主营业务收入\",\"operator\":\"+\"," + + "\"currentAmount\":100.00}]"), + new FmsReportItemRespVO().setRowNo(2).setCurrentAmount(new BigDecimal("40.00")) + .setFormula("[{\"subjectId\":1453,\"subjectNumber\":\"5401\"," + + "\"subjectName\":\"主营业务成本\",\"operator\":\"+\"," + + "\"currentAmount\":40.00}]"), + buildReportItem(3, "0.00"), buildReportItem(11, "0.00"), + buildReportItem(14, "0.00"), buildReportItem(18, "0.00"), + new FmsReportItemRespVO().setRowNo(30).setCurrentAmount(new BigDecimal("60.00")) + .setFormula("[\"L1-L2\"]"))); + + // 调用 + FmsHomeMetricDetailRespVO result = homeService.getMetricDetail(1L, "profit", 10L); + + // 断言 + assertEquals(new BigDecimal("60.00"), result.getTrends().get(11).getAmount()); + assertEquals(1, result.getStructure().size()); + assertEquals("5001", result.getStructure().get(0).getSubjectCode()); + assertEquals(new BigDecimal("100.00"), result.getStructure().get(0).getAmount()); + } + + // ========== 随机对象 ========== + + private List buildIncomeStatement(String month) { + if (!"2026-08".equals(month)) { + return Collections.emptyList(); + } + return Arrays.asList(buildReportItem(1, "100.00"), buildReportItem(2, "40.00"), + buildReportItem(3, "5.00"), buildReportItem(11, "3.00"), + buildReportItem(14, "4.00"), buildReportItem(18, "5.00"), + buildReportItem(30, "30.00")); + } + + private FmsReportItemRespVO buildReportItem(Integer rowNo, String amount) { + return new FmsReportItemRespVO().setRowNo(rowNo).setCurrentAmount(new BigDecimal(amount)); + } + + private List buildMetricIncomeStatement(String month) { + if (!"2026-08".equals(month)) { + return Collections.emptyList(); + } + return Arrays.asList(new FmsReportItemRespVO().setRowNo(1).setCurrentAmount(new BigDecimal("100.00")) + .setFormula("[{\"subjectId\":1444,\"subjectNumber\":\"5001\"," + + "\"subjectName\":\"主营业务收入\",\"operator\":\"+\"," + + "\"currentAmount\":80.00},{\"subjectId\":1445," + + "\"subjectNumber\":\"5051\",\"subjectName\":\"其他业务收入\"," + + "\"operator\":\"+\",\"currentAmount\":20.00}]"), + buildReportItem(2, "40.00"), buildReportItem(3, "5.00"), + buildReportItem(11, "3.00"), buildReportItem(14, "4.00"), + buildReportItem(18, "5.00"), buildReportItem(30, "30.00")); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerServiceImplTest.java new file mode 100644 index 0000000..7572a13 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/ledger/FmsLedgerServiceImplTest.java @@ -0,0 +1,732 @@ +package cn.iocoder.yudao.module.fms.service.ledger; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerAuxiliaryListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.FmsLedgerListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarybalance.FmsLedgerAuxiliaryBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarydetail.FmsLedgerAuxiliaryDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail.FmsLedgerDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.multicolumn.FmsLedgerMultiColumnRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitydetail.FmsLedgerQuantityDetailRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitygeneral.FmsLedgerQuantityGeneralRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsInitialBalanceDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsInitialBalanceMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.voucher.FmsVoucherEntryMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.voucher.FmsVoucherMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.config.FmsVoucherWordMapper; +import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum; +import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryItemService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.LEDGER_PERIOD_BEFORE_ACCOUNT_START; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@Import(FmsLedgerServiceImpl.class) +public class FmsLedgerServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsLedgerServiceImpl ledgerService; + @Resource + private FmsInitialBalanceMapper initialBalanceMapper; + @Resource + private FmsVoucherMapper voucherMapper; + @Resource + private FmsVoucherEntryMapper voucherEntryMapper; + @Resource + private FmsVoucherWordMapper voucherWordMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsFinanceParameterService financeParameterService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsAuxiliaryItemService auxiliaryItemService; + + private FmsSubjectDO cashSubject; + private FmsSubjectDO bankSubject; + private FmsSubjectDO receivableSubject; + private FmsAuxiliaryItemDO customerItem; + + @BeforeEach + public void before() { + cashSubject = buildSubject(101L, "1001", "库存现金", null, 1); + bankSubject = buildSubject(102L, "100201", "工商银行", 101L, 2); + receivableSubject = buildSubject(103L, "1122", "应收账款", null, 1); + customerItem = new FmsAuxiliaryItemDO().setId(301L).setCode("KH001") + .setName("北京星河科技有限公司").setAuxiliaryTypeId(201L) + .setStatus(1).setAccountSetId(1L); + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L) + .setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(accountSet); + when(financeParameterService.getFinanceParameter(1L, 10L)) + .thenReturn(new FmsFinanceParameterDO().setLedgerBalanceMode(1)); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Arrays.asList(cashSubject, bankSubject, receivableSubject)); + when(auxiliaryItemService.getAuxiliaryItemList(1L, 201L, 10L)) + .thenReturn(Collections.singletonList(customerItem)); + when(auxiliaryItemService.getAuxiliaryItemListByAccountSetId(1L, 10L)) + .thenReturn(Collections.singletonList(customerItem)); + } + + @Test + public void testGetDetailLedger() { + // mock 数据 + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(bankSubject.getId()) + .setAccountSetId(1L).setOpeningAmount(new BigDecimal("100.00")) + .setOpeningQuantity(BigDecimal.ZERO).setYearDebitAmount(BigDecimal.ZERO) + .setYearDebitQuantity(BigDecimal.ZERO).setYearCreditAmount(BigDecimal.ZERO) + .setYearCreditQuantity(BigDecimal.ZERO).setYearOpeningAmount(new BigDecimal("100.00")) + .setYearOpeningQuantity(BigDecimal.ZERO).setProfitLossAmount(BigDecimal.ZERO) + .setAuxiliaryAccounting(false)); + FmsVoucherWordDO word = new FmsVoucherWordDO().setName("记").setPrintTitle("记账凭证") + .setDefaultStatus(true).setSort(1).setAccountSetId(1L); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = new FmsVoucherDO().setVoucherWordId(word.getId()).setVoucherNumber(1) + .setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)).setAttachmentUrls(Collections.emptyList()) + .setDebitAmount(new BigDecimal("80.00")).setCreditAmount(new BigDecimal("80.00")) + .setTotal(new BigDecimal("80.00")).setStatus(1).setAccountSetId(1L); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(bankSubject.getId()).setSubjectCode("100201") + .setSubjectName("工商银行").setDigest("收到客户回款").setSort(1) + .setDebitAmount(new BigDecimal("80.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAuxiliaries(Collections.emptyList())); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + reqVO.setSubjectId(bankSubject.getId()); + + // 调用 + List result = ledgerService.getDetailList(reqVO, 10L); + + // 断言 + assertEquals(4, result.size()); + assertEquals(new BigDecimal("100.00"), result.get(0).getBalance()); + assertEquals("记-1", result.get(1).getVoucherNumber()); + assertEquals(new BigDecimal("180.00"), result.get(1).getBalance()); + assertEquals(new BigDecimal("80.00"), result.get(2).getDebitAmount()); + assertEquals("本年累计", result.get(3).getDigest()); + } + + @Test + public void testGetDetailList_startMonthEqualsAccountSetStartMonth() { + // mock 数据 + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(bankSubject.getId()) + .setAccountSetId(1L).setOpeningAmount(new BigDecimal("100.00")) + .setOpeningQuantity(BigDecimal.ZERO).setYearDebitAmount(BigDecimal.ZERO) + .setYearDebitQuantity(BigDecimal.ZERO).setYearCreditAmount(BigDecimal.ZERO) + .setYearCreditQuantity(BigDecimal.ZERO).setYearOpeningAmount(new BigDecimal("100.00")) + .setYearOpeningQuantity(BigDecimal.ZERO).setProfitLossAmount(BigDecimal.ZERO) + .setAuxiliaryAccounting(false)); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + reqVO.setStartMonth("2026-01"); + reqVO.setEndMonth("2026-01"); + reqVO.setSubjectId(bankSubject.getId()); + + // 调用 + List result = ledgerService.getDetailList(reqVO, 10L); + + // 断言 + assertEquals(new BigDecimal("100.00"), CollUtil.getFirst(result).getBalance()); + } + + @Test + public void testGetDetailList_startMonthBeforeAccountSetStartMonth() { + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + reqVO.setStartMonth("2025-12"); + + // 调用并断言 + assertServiceException(() -> ledgerService.getDetailList(reqVO, 10L), + LEDGER_PERIOD_BEFORE_ACCOUNT_START); + } + + @Test + public void testGetQuantityDetailList_filterNonQuantityDescendant() { + // mock 数据:父科目启用数量核算,非数量子科目存在金额分录 + cashSubject.setQuantityAccounting(true); + FmsVoucherWordDO word = buildVoucherWord(); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = buildVoucher(word.getId(), "80.00"); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(bankSubject.getId()).setSubjectCode(bankSubject.getCode()) + .setSubjectName(bankSubject.getName()).setDigest("非数量科目分录").setSort(1) + .setDebitAmount(new BigDecimal("80.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAuxiliaries(Collections.emptyList())); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + reqVO.setSubjectId(cashSubject.getId()); + + // 调用 + List result = ledgerService.getQuantityDetailList(reqVO, 10L); + + // 断言 + assertEquals(3, result.size()); + assertEquals(BigDecimal.ZERO, result.get(1).getDebitAmount()); + assertEquals(BigDecimal.ZERO, result.get(1).getBalance()); + } + + @Test + public void testGetQuantityDetailList_negativeDebitKeepsDebitQuantity() { + // mock 数据:红字借方金额仍属于借方数量,不转换到贷方 + bankSubject.setQuantityAccounting(true); + FmsVoucherWordDO word = buildVoucherWord(); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = buildVoucher(word.getId(), "-20.00"); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(bankSubject.getId()).setSubjectCode(bankSubject.getCode()) + .setSubjectName(bankSubject.getName()).setDigest("红字冲销").setSort(1) + .setDebitAmount(new BigDecimal("-20.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(new BigDecimal("2.00")).setUnitPrice(new BigDecimal("10.00")) + .setAuxiliaries(Collections.emptyList())); + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + reqVO.setSubjectId(bankSubject.getId()); + + // 调用 + List result = ledgerService.getQuantityDetailList(reqVO, 10L); + + // 断言 + FmsLedgerQuantityDetailRespVO voucherRow = CollUtil.findOne(result, + row -> ObjUtil.equal(row.getRowType(), FmsLedgerQuantityDetailRespVO.ROW_TYPE_VOUCHER)); + assertEquals(new BigDecimal("2.0000"), voucherRow.getDebitQuantity()); + assertEquals(BigDecimal.ZERO, voucherRow.getCreditQuantity()); + } + + @Test + public void testGetSubjectBalanceList_endSubjectIncludesChild() { + // mock 数据 + FmsVoucherWordDO word = new FmsVoucherWordDO().setName("记").setPrintTitle("记账凭证") + .setDefaultStatus(true).setSort(1).setAccountSetId(1L); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = new FmsVoucherDO().setVoucherWordId(word.getId()).setVoucherNumber(1) + .setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)).setAttachmentUrls(Collections.emptyList()) + .setDebitAmount(new BigDecimal("80.00")).setCreditAmount(new BigDecimal("80.00")) + .setTotal(new BigDecimal("80.00")).setStatus(1).setAccountSetId(1L); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(bankSubject.getId()).setSubjectCode("100201") + .setSubjectName("工商银行").setDigest("收到客户回款").setSort(1) + .setDebitAmount(new BigDecimal("80.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAuxiliaries(Collections.emptyList())); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + reqVO.setMinLevel(1); + reqVO.setMaxLevel(2); + reqVO.setEndSubjectId(cashSubject.getId()); + + // 调用 + List result = ledgerService.getSubjectBalanceList(reqVO, 10L); + + // 断言 + FmsLedgerSubjectBalanceRespVO parent = CollUtil.getFirst(result); + assertEquals(new BigDecimal("80.00"), parent.getPeriodDebitAmount()); + assertEquals(1, parent.getChildren().size()); + assertEquals(new BigDecimal("80.00"), CollUtil.getFirst(parent.getChildren()).getEndingDebitAmount()); + } + + @Test + public void testGetSubjectBalanceList_filterZeroRows() { + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + + // 调用 + List result = ledgerService.getSubjectBalanceList(reqVO, 10L); + + // 断言 + assertEquals(0, result.size()); + } + + @Test + public void testGetSubjectBalanceList_appendAuxiliaryCombination() { + // mock 数据 + receivableSubject.setAuxiliaryTypeIds(Collections.singletonList(201L)); + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(receivableSubject.getId()) + .setAccountSetId(1L).setAuxiliaryAccounting(true) + .setOpeningAmount(new BigDecimal("40.00")).setOpeningQuantity(BigDecimal.ZERO) + .setYearDebitAmount(BigDecimal.ZERO).setYearDebitQuantity(BigDecimal.ZERO) + .setYearCreditAmount(BigDecimal.ZERO).setYearCreditQuantity(BigDecimal.ZERO) + .setYearOpeningAmount(new BigDecimal("40.00")).setYearOpeningQuantity(BigDecimal.ZERO) + .setProfitLossAmount(BigDecimal.ZERO).setAssistBalances(Collections.singletonList( + FmsInitialBalanceDO.AssistBalance.builder().assistCombinationId(501L) + .auxiliaries(Collections.singletonList(FmsInitialBalanceDO.AuxiliaryItem.builder() + .type(1).typeId(201L).itemId(customerItem.getId()) + .name(customerItem.getName()).build())) + .openingAmount(new BigDecimal("40.00")).openingQuantity(BigDecimal.ZERO) + .yearDebitAmount(BigDecimal.ZERO).yearDebitQuantity(BigDecimal.ZERO) + .yearCreditAmount(BigDecimal.ZERO).yearCreditQuantity(BigDecimal.ZERO).build()))); + FmsVoucherWordDO word = buildVoucherWord(); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = buildVoucher(word.getId(), "100.00"); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(receivableSubject.getId()).setSubjectCode("1122") + .setSubjectName("应收账款").setDigest("确认客户应收款").setSort(1) + .setDebitAmount(new BigDecimal("100.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO).setAssistCombinationId(501L) + .setAuxiliaries(Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .type(1).typeId(201L).itemId(customerItem.getId()) + .name(customerItem.getName()).build()))); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + + // 调用 + List result = ledgerService.getSubjectBalanceList(reqVO, 10L); + + // 断言 + FmsLedgerSubjectBalanceRespVO subjectBalance = CollUtil.getFirst(result); + assertEquals("S:" + receivableSubject.getId(), subjectBalance.getNodeKey()); + assertEquals(FmsLedgerSubjectBalanceRespVO.NODE_TYPE_SUBJECT, subjectBalance.getNodeType()); + assertEquals(1, subjectBalance.getChildren().size()); + FmsLedgerSubjectBalanceRespVO auxiliaryBalance = CollUtil.getFirst(subjectBalance.getChildren()); + assertEquals("A:501", auxiliaryBalance.getNodeKey()); + assertEquals(FmsLedgerSubjectBalanceRespVO.NODE_TYPE_AUXILIARY_COMBINATION, + auxiliaryBalance.getNodeType()); + assertEquals(501L, auxiliaryBalance.getAssistCombinationId()); + assertEquals("1122_KH001", auxiliaryBalance.getSubjectCode()); + assertEquals("应收账款_北京星河科技有限公司", auxiliaryBalance.getSubjectName()); + assertEquals(new BigDecimal("40.00"), auxiliaryBalance.getOpeningDebitAmount()); + assertEquals(new BigDecimal("100.00"), auxiliaryBalance.getPeriodDebitAmount()); + assertEquals(new BigDecimal("140.00"), auxiliaryBalance.getEndingDebitAmount()); + } + + @Test + public void testGetQuantityGeneralList_filterZeroRowsAndKeepQuantity() { + // mock 数据 + bankSubject.setQuantityAccounting(true); + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(bankSubject.getId()) + .setAccountSetId(1L).setOpeningAmount(BigDecimal.ZERO) + .setOpeningQuantity(new BigDecimal("5.00")).setYearDebitAmount(BigDecimal.ZERO) + .setYearDebitQuantity(BigDecimal.ZERO).setYearCreditAmount(BigDecimal.ZERO) + .setYearCreditQuantity(BigDecimal.ZERO).setYearOpeningAmount(BigDecimal.ZERO) + .setYearOpeningQuantity(new BigDecimal("5.00")).setProfitLossAmount(BigDecimal.ZERO) + .setAuxiliaryAccounting(false)); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + + // 调用 + List result = ledgerService.getQuantityGeneralList(reqVO, 10L); + + // 断言 + assertEquals(1, result.size()); + FmsLedgerQuantityGeneralRespVO parent = CollUtil.getFirst(result); + assertEquals(1, parent.getChildren().size()); + assertEquals(new BigDecimal("5.0000"), CollUtil.getFirst(parent.getChildren()).getEndingQuantity()); + } + + @Test + public void testGetQuantityGeneralList_filterYearOnlyRows() { + // mock 数据:本年早期有累计、查询期初和本期均为零 + bankSubject.setQuantityAccounting(true); + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(bankSubject.getId()) + .setAccountSetId(1L).setOpeningAmount(BigDecimal.ZERO).setOpeningQuantity(BigDecimal.ZERO) + .setYearDebitAmount(new BigDecimal("10.00")).setYearDebitQuantity(new BigDecimal("5.00")) + .setYearCreditAmount(new BigDecimal("10.00")).setYearCreditQuantity(new BigDecimal("5.00")) + .setYearOpeningAmount(BigDecimal.ZERO).setYearOpeningQuantity(BigDecimal.ZERO) + .setProfitLossAmount(BigDecimal.ZERO).setAuxiliaryAccounting(false)); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + + // 调用 + List result = ledgerService.getQuantityGeneralList(reqVO, 10L); + + // 断言 + assertEquals(0, result.size()); + } + + @Test + public void testGetQuantityGeneralList_sameAsSubject() { + // mock 数据:借方科目出现贷方净额时,按科目方向保留负数 + bankSubject.setQuantityAccounting(true); + insertQuantityInitialBalance(new BigDecimal("-100.00"), new BigDecimal("-5.00")); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + + // 调用 + List result = ledgerService.getQuantityGeneralList(reqVO, 10L); + + // 断言 + FmsLedgerQuantityGeneralRespVO balance = CollUtil.getFirst(CollUtil.getFirst(result).getChildren()); + assertEquals(FmsDebitCreditDirectionEnum.DEBIT.getName(), balance.getOpeningBalanceDirection()); + assertEquals(new BigDecimal("-100.00"), balance.getOpeningDebitAmount()); + assertEquals(new BigDecimal("-5.0000"), balance.getOpeningQuantity()); + assertEquals(FmsDebitCreditDirectionEnum.DEBIT.getName(), balance.getEndingBalanceDirection()); + assertEquals(new BigDecimal("-100.00"), balance.getEndingDebitAmount()); + assertEquals(new BigDecimal("-5.0000"), balance.getEndingQuantity()); + } + + @Test + public void testGetQuantityGeneralList_oppositeToSubject() { + // mock 数据:按实际净额方向展示时,余额和数量使用绝对值 + bankSubject.setQuantityAccounting(true); + insertQuantityInitialBalance(new BigDecimal("-100.00"), new BigDecimal("-5.00")); + when(financeParameterService.getFinanceParameter(1L, 10L)).thenReturn( + new FmsFinanceParameterDO().setLedgerBalanceMode( + FmsLedgerBalanceModeEnum.OPPOSITE_TO_SUBJECT.getMode())); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + + // 调用 + List result = ledgerService.getQuantityGeneralList(reqVO, 10L); + + // 断言 + FmsLedgerQuantityGeneralRespVO balance = CollUtil.getFirst(CollUtil.getFirst(result).getChildren()); + assertEquals(FmsDebitCreditDirectionEnum.CREDIT.getName(), balance.getOpeningBalanceDirection()); + assertEquals(new BigDecimal("100.00"), balance.getOpeningCreditAmount()); + assertEquals(new BigDecimal("5.0000"), balance.getOpeningQuantity()); + assertEquals(FmsDebitCreditDirectionEnum.CREDIT.getName(), balance.getEndingBalanceDirection()); + assertEquals(new BigDecimal("100.00"), balance.getEndingCreditAmount()); + assertEquals(new BigDecimal("5.0000"), balance.getEndingQuantity()); + } + + @Test + public void testGetMultiColumnLedger() { + // mock 数据 + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(bankSubject.getId()) + .setAccountSetId(1L).setOpeningAmount(new BigDecimal("100.00")) + .setOpeningQuantity(BigDecimal.ZERO).setYearDebitAmount(BigDecimal.ZERO) + .setYearDebitQuantity(BigDecimal.ZERO).setYearCreditAmount(BigDecimal.ZERO) + .setYearCreditQuantity(BigDecimal.ZERO).setYearOpeningAmount(new BigDecimal("100.00")) + .setYearOpeningQuantity(BigDecimal.ZERO).setProfitLossAmount(BigDecimal.ZERO) + .setAuxiliaryAccounting(false)); + FmsVoucherWordDO word = new FmsVoucherWordDO().setName("记").setPrintTitle("记账凭证") + .setDefaultStatus(true).setSort(1).setAccountSetId(1L); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = new FmsVoucherDO().setVoucherWordId(word.getId()).setVoucherNumber(1) + .setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)).setAttachmentUrls(Collections.emptyList()) + .setDebitAmount(new BigDecimal("80.00")).setCreditAmount(new BigDecimal("80.00")) + .setTotal(new BigDecimal("80.00")).setStatus(1).setAccountSetId(1L); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(bankSubject.getId()).setSubjectCode("100201") + .setSubjectName("工商银行").setDigest("收到客户回款").setSort(1) + .setDebitAmount(new BigDecimal("80.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAuxiliaries(Collections.emptyList())); + + // 准备参数 + FmsLedgerListReqVO reqVO = buildListReqVO(); + reqVO.setSubjectId(cashSubject.getId()); + + // 调用 + FmsLedgerMultiColumnRespVO result = ledgerService.getMultiColumn(reqVO, 10L); + + // 断言 + assertEquals(1, result.getColumns().size()); + assertEquals(bankSubject.getId(), CollUtil.getFirst(result.getColumns()).getSubjectId()); + FmsLedgerDetailRespVO voucherRow = CollUtil.findOne(result.getRows(), + row -> row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_VOUCHER)); + assertEquals(new BigDecimal("80.00"), voucherRow.getColumnAmounts().get(bankSubject.getId())); + FmsLedgerDetailRespVO periodTotalRow = CollUtil.findOne(result.getRows(), + row -> row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_PERIOD_TOTAL)); + assertEquals(new BigDecimal("80.00"), periodTotalRow.getColumnAmounts().get(bankSubject.getId())); + FmsLedgerDetailRespVO endingRow = CollUtil.findOne(result.getRows(), + row -> row.getRowType().equals(FmsLedgerDetailRespVO.ROW_TYPE_ENDING)); + assertEquals("期末余额", endingRow.getDigest()); + assertEquals(new BigDecimal("180.00"), endingRow.getBalance()); + assertEquals(new BigDecimal("180.00"), endingRow.getColumnAmounts().get(bankSubject.getId())); + } + + @Test + public void testGetAuxiliaryDetailList() { + // mock 数据 + insertAuxiliaryVoucherEntry(); + // 准备参数 + FmsLedgerAuxiliaryListReqVO reqVO = buildAuxiliaryListReqVO(); + reqVO.setAuxiliaryItemId(customerItem.getId()); + + // 调用 + List result = ledgerService.getAuxiliaryDetailList(reqVO, 10L); + + // 断言 + assertEquals(4, result.size()); + FmsLedgerAuxiliaryDetailRespVO voucherRow = CollUtil.findOne(result, + row -> row.getRowType().equals(FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_VOUCHER)); + assertEquals("记-1", voucherRow.getVoucherNumber()); + assertEquals(new BigDecimal("800.00"), voucherRow.getBalance()); + } + + @Test + public void testGetAuxiliaryDetailList_sameAsSubject() { + // mock 数据 + receivableSubject.setBalanceDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + insertAuxiliaryVoucherEntry(); + // 准备参数 + FmsLedgerAuxiliaryListReqVO reqVO = buildAuxiliaryListReqVO(); + reqVO.setAuxiliaryItemId(customerItem.getId()); + reqVO.setSubjectId(receivableSubject.getId()); + + // 调用 + List result = ledgerService.getAuxiliaryDetailList(reqVO, 10L); + + // 断言 + FmsLedgerAuxiliaryDetailRespVO voucherRow = CollUtil.findOne(result, + row -> row.getRowType().equals(FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_VOUCHER)); + assertEquals(FmsDebitCreditDirectionEnum.CREDIT.getName(), voucherRow.getBalanceDirection()); + assertEquals(new BigDecimal("-800.00"), voucherRow.getBalance()); + } + + @Test + public void testGetAuxiliaryDetailList_oppositeToSubject() { + // mock 数据 + receivableSubject.setBalanceDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + insertAuxiliaryVoucherEntry(); + // 准备参数 + FmsLedgerAuxiliaryListReqVO reqVO = buildAuxiliaryListReqVO(); + reqVO.setAuxiliaryItemId(customerItem.getId()); + reqVO.setSubjectId(receivableSubject.getId()); + + // mock 财务参数为按实际净额方向展示 + when(financeParameterService.getFinanceParameter(1L, 10L)).thenReturn( + new FmsFinanceParameterDO().setLedgerBalanceMode( + FmsLedgerBalanceModeEnum.OPPOSITE_TO_SUBJECT.getMode())); + + // 调用 + List result = ledgerService.getAuxiliaryDetailList(reqVO, 10L); + + // 断言 + FmsLedgerAuxiliaryDetailRespVO voucherRow = CollUtil.findOne(result, + row -> row.getRowType().equals(FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_VOUCHER)); + assertEquals(FmsDebitCreditDirectionEnum.DEBIT.getName(), voucherRow.getBalanceDirection()); + assertEquals(new BigDecimal("800.00"), voucherRow.getBalance()); + } + + @Test + public void testGetAuxiliaryDetailList_withoutSubject() { + // mock 数据 + receivableSubject.setBalanceDirection(FmsDebitCreditDirectionEnum.CREDIT.getType()); + insertAuxiliaryVoucherEntry(); + // 准备参数:不指定科目时聚合多个科目,不任取其中一个科目方向 + FmsLedgerAuxiliaryListReqVO reqVO = buildAuxiliaryListReqVO(); + reqVO.setAuxiliaryItemId(customerItem.getId()); + + // 调用 + List result = ledgerService.getAuxiliaryDetailList(reqVO, 10L); + + // 断言 + FmsLedgerAuxiliaryDetailRespVO voucherRow = CollUtil.findOne(result, + row -> row.getRowType().equals(FmsLedgerAuxiliaryDetailRespVO.ROW_TYPE_VOUCHER)); + assertEquals(FmsDebitCreditDirectionEnum.DEBIT.getName(), voucherRow.getBalanceDirection()); + assertEquals(new BigDecimal("800.00"), voucherRow.getBalance()); + } + + @Test + public void testGetAuxiliaryBalanceList() { + // mock 数据 + insertAuxiliaryVoucherEntry(); + // 准备参数 + FmsLedgerAuxiliaryListReqVO reqVO = buildAuxiliaryListReqVO(); + + // 调用 + List result = ledgerService.getAuxiliaryBalanceList(reqVO, 10L); + + // 断言 + FmsLedgerAuxiliaryBalanceRespVO balance = CollUtil.getFirst(result); + assertEquals(customerItem.getName(), balance.getName()); + assertEquals(new BigDecimal("800.00"), balance.getPeriodDebitAmount()); + assertEquals(new BigDecimal("800.00"), balance.getEndingDebitAmount()); + } + + @Test + public void testGetAuxiliaryBalanceList_filterZeroRows() { + // mock 数据 + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(receivableSubject.getId()) + .setAccountSetId(1L).setAuxiliaryAccounting(true) + .setOpeningAmount(BigDecimal.ZERO).setOpeningQuantity(BigDecimal.ZERO) + .setYearDebitAmount(BigDecimal.ZERO).setYearDebitQuantity(BigDecimal.ZERO) + .setYearCreditAmount(BigDecimal.ZERO).setYearCreditQuantity(BigDecimal.ZERO) + .setYearOpeningAmount(BigDecimal.ZERO).setYearOpeningQuantity(BigDecimal.ZERO) + .setProfitLossAmount(BigDecimal.ZERO).setAssistBalances(Collections.singletonList( + FmsInitialBalanceDO.AssistBalance.builder() + .assistCombinationId(501L) + .auxiliaries(Collections.singletonList( + FmsInitialBalanceDO.AuxiliaryItem.builder() + .type(1).typeId(201L).itemId(customerItem.getId()) + .name(customerItem.getName()).build())) + .openingAmount(BigDecimal.ZERO).openingQuantity(BigDecimal.ZERO) + .yearDebitAmount(BigDecimal.ZERO).yearDebitQuantity(BigDecimal.ZERO) + .yearCreditAmount(BigDecimal.ZERO).yearCreditQuantity(BigDecimal.ZERO) + .yearOpeningAmount(BigDecimal.ZERO).yearOpeningQuantity(BigDecimal.ZERO) + .profitLossAmount(BigDecimal.ZERO).profitLossQuantity(BigDecimal.ZERO).build()))); + + // 准备参数 + FmsLedgerAuxiliaryListReqVO reqVO = buildAuxiliaryListReqVO(); + + // 调用 + List result = ledgerService.getAuxiliaryBalanceList(reqVO, 10L); + + // 断言 + assertEquals(0, result.size()); + } + + @Test + public void testGetAuxiliaryBalanceList_startMonthBeforeAccountSetStartMonth() { + // 准备参数 + FmsLedgerAuxiliaryListReqVO reqVO = buildAuxiliaryListReqVO(); + reqVO.setStartMonth("2025-12"); + + // 调用并断言 + assertServiceException(() -> ledgerService.getAuxiliaryBalanceList(reqVO, 10L), + LEDGER_PERIOD_BEFORE_ACCOUNT_START); + } + + @Test + public void testGetAuxiliaryCombinationBalance() { + // mock 数据 + receivableSubject.setAuxiliaryTypeIds(Collections.singletonList(201L)); + when(auxiliaryItemService.validateAuxiliaryItemList( + 1L, Collections.singletonList(customerItem.getId()))) + .thenReturn(Collections.singletonList(customerItem)); + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(receivableSubject.getId()) + .setAccountSetId(1L).setAuxiliaryAccounting(true) + .setOpeningAmount(new BigDecimal("40.00")).setOpeningQuantity(BigDecimal.ZERO) + .setYearDebitAmount(BigDecimal.ZERO).setYearDebitQuantity(BigDecimal.ZERO) + .setYearCreditAmount(BigDecimal.ZERO).setYearCreditQuantity(BigDecimal.ZERO) + .setYearOpeningAmount(new BigDecimal("40.00")).setYearOpeningQuantity(BigDecimal.ZERO) + .setProfitLossAmount(BigDecimal.ZERO).setAssistBalances(Collections.singletonList( + FmsInitialBalanceDO.AssistBalance.builder() + .assistCombinationId(501L) + .auxiliaries(Collections.singletonList( + FmsInitialBalanceDO.AuxiliaryItem.builder() + .type(1).typeId(201L).itemId(customerItem.getId()) + .name(customerItem.getName()).build())) + .openingAmount(new BigDecimal("40.00")).build()))); + FmsVoucherWordDO word = buildVoucherWord(); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = buildVoucher(word.getId(), "100.00"); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(receivableSubject.getId()).setSubjectCode("1122") + .setSubjectName("应收账款").setDigest("客户一应收款").setSort(1) + .setDebitAmount(new BigDecimal("100.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAuxiliaries(Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .type(1).typeId(201L).itemId(customerItem.getId()) + .name(customerItem.getName()).build()))); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(receivableSubject.getId()).setSubjectCode("1122") + .setSubjectName("应收账款").setDigest("客户二冲减").setSort(2) + .setDebitAmount(BigDecimal.ZERO).setCreditAmount(new BigDecimal("100.00")) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAuxiliaries(Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .type(1).typeId(201L).itemId(302L).name("上海客户").build()))); + + // 调用 + BigDecimal balance = ledgerService.getAuxiliaryCombinationBalance( + 1L, "2026-08", receivableSubject.getId(), + Collections.singletonList(customerItem.getId()), 10L); + + // 断言 + assertEquals(new BigDecimal("140.00"), balance); + } + + // ========== 随机对象 ========== + + private FmsLedgerListReqVO buildListReqVO() { + FmsLedgerListReqVO reqVO = new FmsLedgerListReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setStartMonth("2026-08"); + reqVO.setEndMonth("2026-08"); + return reqVO; + } + + private FmsLedgerAuxiliaryListReqVO buildAuxiliaryListReqVO() { + FmsLedgerAuxiliaryListReqVO reqVO = new FmsLedgerAuxiliaryListReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setStartMonth("2026-08"); + reqVO.setEndMonth("2026-08"); + reqVO.setAuxiliaryTypeId(201L); + return reqVO; + } + + private FmsVoucherWordDO buildVoucherWord() { + return randomPojo(FmsVoucherWordDO.class, voucherWord -> voucherWord.setId(null) + .setName("记").setPrintTitle("记账凭证").setDefaultStatus(true) + .setSort(1).setAccountSetId(1L)); + } + + private FmsVoucherDO buildVoucher(Long voucherWordId, String amount) { + return randomPojo(FmsVoucherDO.class, voucher -> voucher.setId(null) + .setVoucherWordId(voucherWordId).setVoucherNumber(1) + .setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)).setAttachmentUrls(Collections.emptyList()) + .setDebitAmount(new BigDecimal(amount)).setCreditAmount(new BigDecimal(amount)) + .setTotal(new BigDecimal(amount)).setStatus(1).setAccountSetId(1L)); + } + + private void insertQuantityInitialBalance(BigDecimal openingAmount, BigDecimal openingQuantity) { + initialBalanceMapper.insert(new FmsInitialBalanceDO().setSubjectId(bankSubject.getId()) + .setAccountSetId(1L).setOpeningAmount(openingAmount).setOpeningQuantity(openingQuantity) + .setYearDebitAmount(BigDecimal.ZERO).setYearDebitQuantity(BigDecimal.ZERO) + .setYearCreditAmount(BigDecimal.ZERO).setYearCreditQuantity(BigDecimal.ZERO) + .setYearOpeningAmount(openingAmount).setYearOpeningQuantity(openingQuantity) + .setProfitLossAmount(BigDecimal.ZERO).setAuxiliaryAccounting(false)); + } + + private void insertAuxiliaryVoucherEntry() { + FmsVoucherWordDO word = buildVoucherWord(); + voucherWordMapper.insert(word); + FmsVoucherDO voucher = buildVoucher(word.getId(), "800.00"); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(voucher.getId()) + .setAccountSetId(1L).setSubjectId(receivableSubject.getId()).setSubjectCode("1122") + .setSubjectName("应收账款").setDigest("确认客户应收款").setSort(1) + .setDebitAmount(new BigDecimal("800.00")).setCreditAmount(BigDecimal.ZERO) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAuxiliaries(Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .type(1).typeId(201L).itemId(customerItem.getId()) + .name(customerItem.getName()).build()))); + } + + private FmsSubjectDO buildSubject(Long id, String code, String name, Long parentId, Integer level) { + return randomPojo(FmsSubjectDO.class, subject -> subject.setId(id) + .setCode(code).setName(name).setParentId(parentId) + .setType(1).setCategory(1).setBalanceDirection(1).setQuantityAccounting(false) + .setCash(false).setStatus(1).setLevel(level).setAccountSetId(1L) + .setAuxiliaryTypeIds(Collections.emptyList()).setCurrencyIds(Collections.emptyList())); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetServiceImplTest.java new file mode 100644 index 0000000..6cad241 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsBalanceSheetServiceImplTest.java @@ -0,0 +1,264 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.balance.FmsBalanceSheetConfigDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.balance.FmsBalanceSheetReportDO; +import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetConfigMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetReportMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum; +import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService; +import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import({FmsBalanceSheetServiceImpl.class, FmsReportCommonServiceImpl.class}) +public class FmsBalanceSheetServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsBalanceSheetServiceImpl balanceSheetService; + @Resource + private FmsReportTemplateMapper reportTemplateMapper; + @Resource + private FmsBalanceSheetConfigMapper balanceSheetConfigMapper; + @Resource + private FmsBalanceSheetReportMapper balanceSheetReportMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsLedgerService ledgerService; + @MockitoBean + private FmsInitialBalanceService initialBalanceService; + @MockitoBean + private FmsFinanceParameterService financeParameterService; + @MockitoBean + private FmsClosingPeriodService closingPeriodService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + @MockitoBean + private FmsClosingVoucherService closingVoucherService; + @MockitoBean + private FmsVoucherService voucherService; + + private FmsSubjectDO cashSubject; + + @BeforeEach + public void before() { + cashSubject = new FmsSubjectDO().setId(103L).setCode("1001").setName("库存现金") + .setBalanceDirection(1).setLevel(1).setAccountSetId(1L); + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L) + .setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn(accountSet); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(accountSet); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Collections.singletonList(cashSubject)); + when(financeParameterService.getFinanceParameter(1L)).thenReturn(new FmsFinanceParameterDO() + .setSubjectCodeRule(FmsFinanceParameterDO.DEFAULT_SUBJECT_CODE_RULE)); + when(financeParameterService.convertStandardSubjectCode(any(), any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + public void testGetBalanceSheet_pairRowsAndLineFormula() { + // mock 数据 + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("流动资产:").setRowNo(0) + .setFormula("[]").setEditable(false).setSort(0).setRowId(0) + .setType(FmsReportTypeEnum.BALANCE_SHEET.getType()).setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("流动负债:").setRowNo(0) + .setFormula("[]").setEditable(false).setSort(32).setRowId(0) + .setType(FmsReportTypeEnum.BALANCE_SHEET.getType()).setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("货币资金").setRowNo(1) + .setFormula("[{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"1001\"}]") + .setEditable(true).setSort(1).setRowId(1) + .setType(FmsReportTypeEnum.BALANCE_SHEET.getType()).setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("资产总计").setRowNo(30) + .setFormula("[\"L1\"]").setEditable(false).setSort(2).setRowId(2) + .setType(FmsReportTypeEnum.BALANCE_SHEET.getType()).setLevel(1)); + FmsLedgerSubjectBalanceRespVO balance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(103L) + .setSubjectCode("1001").setOpeningDebitAmount(BigDecimal.ZERO) + .setOpeningCreditAmount(new BigDecimal("100.00")) + .setEndingDebitAmount(new BigDecimal("900.00")).setEndingCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(balance)); + + // 准备参数 + FmsReportListReqVO reqVO = buildQueryReqVO(); + + // 调用 + List result = balanceSheetService.getBalanceSheet(reqVO, 10L); + + // 断言 + assertEquals(3, result.size()); + assertNotNull(CollUtil.getFirst(result).getAssetName()); + assertNotNull(CollUtil.getFirst(result).getLiabilityName()); + assertEquals(new BigDecimal("900.00"), result.get(1).getAssetClosingAmount()); + assertEquals(new BigDecimal("900.00"), result.get(2).getAssetClosingAmount()); + } + + @Test + public void testUpdateBalanceSheetFormula() { + // mock 数据 + FmsBalanceSheetConfigDO config = new FmsBalanceSheetConfigDO().setName("货币资金").setRowNo(1) + .setFormula("[]").setEditable(true).setSort(1).setAccountSetId(1L).setLevel(2).setRowId(1); + balanceSheetConfigMapper.insert(config); + FmsBalanceSheetReportDO report = new FmsBalanceSheetReportDO().setName("货币资金").setRowNo(1) + .setFormula("[]").setEditable(true).setSort(1).setAccountSetId(1L).setLevel(2).setRowId(1) + .setFromPeriod(202608).setToPeriod(202608).setType(1) + .setOpeningAmount(BigDecimal.ZERO).setClosingAmount(BigDecimal.ZERO).setSettled(false); + balanceSheetReportMapper.insert(report); + + // 准备参数 + FmsReportFormulaUpdateReqVO.Formula formulaReqVO = new FmsReportFormulaUpdateReqVO.Formula(); + formulaReqVO.setSubjectId(cashSubject.getId()); + formulaReqVO.setOperator("-"); + formulaReqVO.setRules(1); + FmsReportFormulaUpdateReqVO reqVO = new FmsReportFormulaUpdateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setId(report.getId()); + reqVO.setFormulas(Collections.singletonList(formulaReqVO)); + + // 调用 + balanceSheetService.updateBalanceSheetFormula(reqVO, 10L); + + // 断言 + FmsBalanceSheetConfigDO updateConfig = balanceSheetConfigMapper.selectById(config.getId()); + List formulas = JsonUtils.parseArray(updateConfig.getFormula(), FmsReportFormulaRespVO.class); + assertEquals(1, formulas.size()); + assertEquals(cashSubject.getId(), CollUtil.getFirst(formulas).getSubjectId()); + assertEquals(cashSubject.getCode(), CollUtil.getFirst(formulas).getSubjectNumber()); + assertEquals(cashSubject.getName(), CollUtil.getFirst(formulas).getSubjectName()); + assertEquals("-", CollUtil.getFirst(formulas).getOperator()); + assertEquals(1, CollUtil.getFirst(formulas).getRules()); + assertEquals(updateConfig.getFormula(), balanceSheetReportMapper.selectById(report.getId()).getFormula()); + } + + @Test + public void testCheckBalanceSheet() { + // mock 数据 + FmsSubjectDO liabilitySubject = new FmsSubjectDO().setId(104L).setCode("2001").setName("短期借款") + .setBalanceDirection(2).setLevel(1).setAccountSetId(1L) + .setType(FmsSubjectTypeEnum.LIABILITY.getType()); + cashSubject.setType(FmsSubjectTypeEnum.ASSET.getType()); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Arrays.asList(cashSubject, liabilitySubject)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("货币资金").setRowNo(1) + .setFormula("[{\"operator\":\"+\",\"rules\":0,\"subjectNumber\":\"1001\"}]") + .setEditable(true).setSort(1).setRowId(1) + .setType(FmsReportTypeEnum.BALANCE_SHEET.getType()).setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("资产总计").setRowNo(30) + .setFormula("[\"L1\"]").setEditable(false).setSort(2).setRowId(2) + .setType(FmsReportTypeEnum.BALANCE_SHEET.getType()).setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("负债和所有者权益总计").setRowNo(53) + .setFormula("[]").setEditable(false).setSort(33).setRowId(3) + .setType(FmsReportTypeEnum.BALANCE_SHEET.getType()).setLevel(1)); + FmsLedgerSubjectBalanceRespVO cashBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(103L) + .setSubjectCode("1001").setOpeningDebitAmount(new BigDecimal("100.00")) + .setOpeningCreditAmount(BigDecimal.ZERO).setEndingDebitAmount(new BigDecimal("110.00")) + .setEndingCreditAmount(BigDecimal.ZERO).setChildren(Collections.emptyList()); + FmsLedgerSubjectBalanceRespVO liabilityBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(104L) + .setSubjectCode("2001").setOpeningDebitAmount(BigDecimal.ZERO) + .setOpeningCreditAmount(new BigDecimal("90.00")).setEndingDebitAmount(BigDecimal.ZERO) + .setEndingCreditAmount(new BigDecimal("95.00")).setChildren(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Arrays.asList(cashBalance, liabilityBalance)); + FmsTrialBalanceRespVO trialBalance = new FmsTrialBalanceRespVO(); + trialBalance.setBalanced(false); + when(initialBalanceService.getTrialBalance(1L, 10L)).thenReturn(trialBalance); + when(closingPeriodService.getClosingOverview(any(), eq(10L))) + .thenReturn(new FmsClosingOverviewRespVO().setProfitLossBalance(new BigDecimal("20.00"))); + + // 调用 + FmsBalanceSheetCheckRespVO result = balanceSheetService.checkBalanceSheet(buildQueryReqVO(), 10L); + + // 断言 + assertFalse(result.getBalanced()); + assertFalse(result.getInitialBalanceBalanced()); + assertFalse(result.getProfitLossTransferred()); + assertEquals(new BigDecimal("100.00"), result.getOpeningDifferenceAmount()); + assertEquals(new BigDecimal("110.00"), result.getClosingDifferenceAmount()); + assertEquals(1, result.getUnmappedSubjects().size()); + assertEquals(liabilitySubject.getId(), CollUtil.getFirst(result.getUnmappedSubjects()).getId()); + } + + @Test + public void testCheckBalanceSheet_capitalizedExpenseWithExpandedCodeRule() { + // mock 数据 + FmsSubjectDO capitalizedExpenseSubject = new FmsSubjectDO().setId(104L).setCode("40404") + .setName("资本化支出").setBalanceDirection(1).setLevel(1).setAccountSetId(1L) + .setType(FmsSubjectTypeEnum.ASSET.getType()); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Collections.singletonList(capitalizedExpenseSubject)); + when(financeParameterService.getFinanceParameter(1L)).thenReturn(new FmsFinanceParameterDO() + .setSubjectCodeRule("5-2-2-2")); + when(financeParameterService.convertStandardSubjectCode("4404", "5-2-2-2")) + .thenReturn("40404"); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(Collections.singletonList( + new FmsLedgerSubjectBalanceRespVO().setSubjectId(104L).setSubjectCode("40404") + .setOpeningDebitAmount(new BigDecimal("100.00")).setOpeningCreditAmount(BigDecimal.ZERO) + .setEndingDebitAmount(new BigDecimal("100.00")).setEndingCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()))); + when(initialBalanceService.getTrialBalance(1L, 10L)) + .thenReturn(new FmsTrialBalanceRespVO().setBalanced(true)); + when(closingPeriodService.getClosingOverview(any(), eq(10L))) + .thenReturn(new FmsClosingOverviewRespVO().setProfitLossBalance(BigDecimal.ZERO)); + + // 调用 + FmsBalanceSheetCheckRespVO result = balanceSheetService.checkBalanceSheet(buildQueryReqVO(), 10L); + + // 断言 + assertEquals(0, result.getUnmappedSubjects().size()); + } + + // ========== 随机对象 ========== + + private FmsReportListReqVO buildQueryReqVO() { + FmsReportListReqVO reqVO = new FmsReportListReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setStartMonth("2026-08"); + reqVO.setEndMonth("2026-08"); + return reqVO; + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementServiceImplTest.java new file mode 100644 index 0000000..c46a86c --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsCashFlowStatementServiceImplTest.java @@ -0,0 +1,280 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowAdjustmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.cashflow.FmsCashFlowStatementUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowExtendDataDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.cashflow.FmsCashFlowStatementReportDO; +import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow.FmsCashFlowExtendDataMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.cashflow.FmsCashFlowStatementReportMapper; +import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import({FmsCashFlowStatementServiceImpl.class, FmsReportCommonServiceImpl.class}) +public class FmsCashFlowStatementServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsCashFlowStatementServiceImpl cashFlowStatementService; + @Resource + private FmsReportTemplateMapper reportTemplateMapper; + @Resource + private FmsCashFlowExtendDataMapper cashFlowExtendDataMapper; + @Resource + private FmsCashFlowStatementReportMapper cashFlowStatementReportMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsLedgerService ledgerService; + @MockitoBean + private FmsBalanceSheetService balanceSheetService; + @MockitoBean + private FmsIncomeStatementService incomeStatementService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + + private FmsSubjectDO expenseSubject; + + @BeforeEach + public void before() { + expenseSubject = new FmsSubjectDO().setId(102L).setCode("5601").setName("销售费用") + .setBalanceDirection(1).setLevel(1).setAccountSetId(1L); + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L) + .setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn(accountSet); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(accountSet); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Collections.singletonList(expenseSubject)); + } + + @Test + public void testGetCashFlowStatement_crossReportFormula() { + // mock 数据 + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("销售商品收到的现金").setRowNo(1) + .setFormula("[\"IN1\"]").setEditable(false).setSort(0) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("支付其他经营活动现金").setRowNo(6) + .setFormula("[]").setEditable(false).setSort(1) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("经营活动现金流量净额").setRowNo(7) + .setFormula("[\"L1-L6\"]").setEditable(false).setSort(2) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("现金净增加额").setRowNo(20) + .setFormula("[\"L7\"]").setEditable(false).setSort(3) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("期初现金余额").setRowNo(21) + .setFormula("[\"BA[1,1]\"]").setEditable(false).setSort(4) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("期末现金余额").setRowNo(22) + .setFormula("[\"L20+L21\"]").setEditable(false).setSort(5) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(1)); + Map balanceLineMap = new HashMap<>(); + balanceLineMap.put(FmsBalanceSheetServiceImpl.MONETARY_CASH_ROW_NO, new FmsReportItemRespVO() + .setRowNo(FmsBalanceSheetServiceImpl.MONETARY_CASH_ROW_NO) + .setOpeningAmount(new BigDecimal("100.00")).setClosingAmount(new BigDecimal("1100.00"))); + when(balanceSheetService.getBalanceSheetLineMap(any(), any(), any(), eq(10L))) + .thenReturn(balanceLineMap); + when(incomeStatementService.getIncomeStatement(any(), eq(10L))) + .thenReturn(Collections.singletonList(new FmsReportItemRespVO().setRowNo(1) + .setCurrentAmount(new BigDecimal("1000.00")) + .setYearAmount(new BigDecimal("1000.00")))); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.emptyList()); + + // 准备参数 + FmsReportListReqVO reqVO = buildQueryReqVO(); + + // 调用 + List result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L); + + // 断言 + assertEquals(6, result.size()); + assertEquals(new BigDecimal("1000.00"), result.get(0).getCurrentAmount()); + assertEquals(new BigDecimal("1100.00"), result.get(5).getCurrentAmount()); + assertEquals(new BigDecimal("1100.00"), result.get(5).getYearAmount()); + assertEquals(6, cashFlowStatementReportMapper.selectListByPeriod( + 1L, 202608, 202608, 1).size()); + } + + @Test + public void testUpdateCashFlowStatement() { + // mock 数据 + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("经营活动现金流入").setRowNo(1) + .setFormula("[\"IN1\"]").setEditable(false).setSort(0) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("经营活动现金流量净额").setRowNo(2) + .setFormula("[\"L1\"]").setEditable(false).setSort(1) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setLevel(1)); + when(balanceSheetService.getBalanceSheetLineMap(any(), any(), any(), eq(10L))) + .thenReturn(Collections.emptyMap()); + when(incomeStatementService.getIncomeStatement(any(), eq(10L))) + .thenReturn(Collections.singletonList(new FmsReportItemRespVO().setRowNo(1) + .setCurrentAmount(new BigDecimal("100.00")) + .setYearAmount(new BigDecimal("150.00")))); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.emptyList()); + + // 准备参数 + FmsReportListReqVO listReqVO = buildQueryReqVO(); + List reports = cashFlowStatementService.getCashFlowStatement( + listReqVO, 10L); + FmsCashFlowStatementUpdateReqVO updateReqVO = new FmsCashFlowStatementUpdateReqVO() + .setAccountSetId(1L).setStartMonth("2026-08").setEndMonth("2026-08") + .setItems(Collections.singletonList(new FmsCashFlowStatementUpdateReqVO.Item() + .setId(reports.get(0).getId()).setCurrentAmount(new BigDecimal("80.00")) + .setYearAmount(new BigDecimal("200.00")))); + + // 调用 + cashFlowStatementService.updateCashFlowStatement(updateReqVO, 10L); + reports = cashFlowStatementService.getCashFlowStatement(listReqVO, 10L); + + // 断言保存金额和后续行次联动 + assertEquals(new BigDecimal("80.00"), reports.get(0).getCurrentAmount()); + assertEquals(new BigDecimal("200.00"), reports.get(0).getYearAmount()); + assertEquals(new BigDecimal("80.00"), reports.get(1).getCurrentAmount()); + assertEquals(new BigDecimal("200.00"), reports.get(1).getYearAmount()); + + // 调用:金额清零后重新按公式计算 + updateReqVO.getItems().get(0).setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO); + cashFlowStatementService.updateCashFlowStatement(updateReqVO, 10L); + reports = cashFlowStatementService.getCashFlowStatement(listReqVO, 10L); + + // 断言 + assertEquals(new BigDecimal("100.00"), reports.get(0).getCurrentAmount()); + assertEquals(new BigDecimal("150.00"), reports.get(0).getYearAmount()); + } + + @Test + public void testGetCashFlowAdjustmentListAndStatement() { + // mock 数据 + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("支付给职工的工资").setRowNo(1) + .setFormula("[{\"operator\":\"+\",\"rules\":5,\"subjectNumber\":\"5601\"}]") + .setEditable(true).setSort(0) + .setType(FmsReportTypeEnum.CASH_FLOW_ADJUSTMENT.getType()).setCategory(1).setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("支付给职工的其他福利费").setRowNo(2) + .setFormula("[]").setEditable(true).setSort(1) + .setType(FmsReportTypeEnum.CASH_FLOW_ADJUSTMENT.getType()).setCategory(1).setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("支付给职工以及为职工支付的现金") + .setRowNo(4).setFormula("[\"L1+L2\"]").setEditable(false).setSort(2) + .setType(FmsReportTypeEnum.CASH_FLOW_ADJUSTMENT.getType()).setCategory(1).setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("支付的职工薪酬").setRowNo(4) + .setFormula("[\"EX4\"]").setEditable(false).setSort(0) + .setType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).setCategory(1).setLevel(2)); + FmsLedgerSubjectBalanceRespVO expenseBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(102L) + .setSubjectCode("5601").setPeriodDebitAmount(new BigDecimal("200.00")) + .setPeriodCreditAmount(new BigDecimal("20.00")).setYearDebitAmount(new BigDecimal("500.00")) + .setYearCreditAmount(new BigDecimal("50.00")).setChildren(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(expenseBalance)); + when(balanceSheetService.getBalanceSheetLineMap(any(), any(), any(), eq(10L))) + .thenReturn(Collections.emptyMap()); + when(incomeStatementService.getIncomeStatement(any(), eq(10L))) + .thenReturn(Collections.emptyList()); + + // 调用 + List adjustments = cashFlowStatementService.getCashFlowAdjustmentList( + buildQueryReqVO(), 10L); + + // 断言 + assertEquals(3, adjustments.size()); + assertEquals(new BigDecimal("200.00"), adjustments.get(0).getCurrentAmount()); + assertEquals(new BigDecimal("500.00"), adjustments.get(0).getYearAmount()); + assertEquals(new BigDecimal("200.00"), adjustments.get(2).getCurrentAmount()); + + // 准备参数 + FmsCashFlowExtendDataDO manualData = cashFlowExtendDataMapper.selectListByPeriod( + 1L, 202608, 202608, 1, 1).get(1); + cashFlowStatementService.getCashFlowStatement(buildQueryReqVO(), 10L); + FmsCashFlowStatementReportDO adjustedReport = CollUtil.getFirst( + cashFlowStatementReportMapper.selectListByPeriod(1L, 202608, 202608, 1)); + cashFlowStatementReportMapper.updateById(new FmsCashFlowStatementReportDO().setId(adjustedReport.getId()) + .setCurrentAmount(new BigDecimal("999.00")).setYearAmount(new BigDecimal("999.00"))); + FmsCashFlowAdjustmentUpdateReqVO.Item itemReqVO = new FmsCashFlowAdjustmentUpdateReqVO.Item(); + itemReqVO.setId(manualData.getId()); + itemReqVO.setCurrentAmount(new BigDecimal("30.00")); + itemReqVO.setYearAmount(new BigDecimal("60.00")); + FmsCashFlowAdjustmentUpdateReqVO updateReqVO = new FmsCashFlowAdjustmentUpdateReqVO(); + updateReqVO.setAccountSetId(1L); + updateReqVO.setItems(Collections.singletonList(itemReqVO)); + + // 调用 + cashFlowStatementService.updateCashFlowAdjustment(updateReqVO, 10L); + adjustedReport = cashFlowStatementReportMapper.selectById(adjustedReport.getId()); + assertEquals(0, BigDecimal.ZERO.compareTo(adjustedReport.getCurrentAmount())); + assertEquals(0, BigDecimal.ZERO.compareTo(adjustedReport.getYearAmount())); + adjustments = cashFlowStatementService.getCashFlowAdjustmentList(buildQueryReqVO(), 10L); + List statement = cashFlowStatementService.getCashFlowStatement( + buildQueryReqVO(), 10L); + + // 断言 + assertEquals(new BigDecimal("230.00"), adjustments.get(2).getCurrentAmount()); + assertEquals(new BigDecimal("560.00"), adjustments.get(2).getYearAmount()); + assertEquals(new BigDecimal("230.00"), CollUtil.getFirst(statement).getCurrentAmount()); + assertEquals(new BigDecimal("560.00"), CollUtil.getFirst(statement).getYearAmount()); + } + + @Test + public void testCheckCashFlowStatement() { + // mock 数据 + FmsBalanceSheetCheckRespVO balanceCheck = new FmsBalanceSheetCheckRespVO() + .setBalanced(true).setInitialBalanceBalanced(true).setProfitLossTransferred(true) + .setOpeningDifferenceAmount(BigDecimal.ZERO).setClosingDifferenceAmount(BigDecimal.ZERO) + .setUnmappedSubjects(Collections.emptyList()); + when(balanceSheetService.checkBalanceSheet(any(), eq(10L))).thenReturn(balanceCheck); + + // 调用 + FmsCashFlowCheckRespVO result = cashFlowStatementService.checkCashFlowStatement( + buildQueryReqVO(), 10L); + + // 断言 + assertTrue(result.getBalanced()); + assertTrue(result.getInitialBalanceBalanced()); + assertTrue(result.getProfitLossTransferred()); + assertTrue(result.getBalanceSheetReady()); + } + + // ========== 随机对象 ========== + + private FmsReportListReqVO buildQueryReqVO() { + FmsReportListReqVO reqVO = new FmsReportListReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setStartMonth("2026-08"); + reqVO.setEndMonth("2026-08"); + return reqVO; + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementServiceImplTest.java new file mode 100644 index 0000000..1e09cf3 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsIncomeStatementServiceImplTest.java @@ -0,0 +1,334 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.income.FmsIncomeStatementCheckRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportFormulaUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.income.FmsIncomeStatementConfigDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.report.income.FmsIncomeStatementReportDO; +import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.income.FmsIncomeStatementConfigMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.report.income.FmsIncomeStatementReportMapper; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_CONFIG_NOT_EDITABLE; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.REPORT_FORMULA_RULE_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import({FmsIncomeStatementServiceImpl.class, FmsReportCommonServiceImpl.class}) +public class FmsIncomeStatementServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsIncomeStatementServiceImpl incomeStatementService; + @Resource + private FmsReportTemplateMapper reportTemplateMapper; + @Resource + private FmsIncomeStatementConfigMapper incomeStatementConfigMapper; + @Resource + private FmsIncomeStatementReportMapper incomeStatementReportMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsLedgerService ledgerService; + @MockitoBean + private FmsBalanceSheetService balanceSheetService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + @MockitoBean + private FmsClosingVoucherService closingVoucherService; + @MockitoBean + private FmsVoucherService voucherService; + + private FmsSubjectDO revenueSubject; + private FmsSubjectDO expenseSubject; + + @BeforeEach + public void before() { + revenueSubject = new FmsSubjectDO().setId(101L).setCode("5001").setName("主营业务收入") + .setBalanceDirection(2).setLevel(1).setAccountSetId(1L); + expenseSubject = new FmsSubjectDO().setId(102L).setCode("5601").setName("销售费用") + .setBalanceDirection(1).setLevel(1).setAccountSetId(1L); + FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L) + .setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn(accountSet); + when(accountSetService.validateAccountSetWritePermission(1L, 10L)).thenReturn(accountSet); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Arrays.asList(revenueSubject, expenseSubject)); + } + + @Test + public void testGetIncomeStatement() { + // mock 数据 + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("一、营业收入").setRowNo(1) + .setFormula("[{\"operator\":\"+\",\"rules\":6,\"subjectNumber\":\"5001\"}]") + .setEditable(true).setSort(0).setType(FmsReportTypeEnum.INCOME_STATEMENT.getType()) + .setLevel(1)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("减:销售费用").setRowNo(2) + .setFormula("[{\"operator\":\"+\",\"rules\":5,\"subjectNumber\":\"5601\"}]") + .setEditable(true).setSort(1).setType(FmsReportTypeEnum.INCOME_STATEMENT.getType()) + .setLevel(2)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("二、营业利润").setRowNo(3) + .setFormula("[\"L1-L2\"]").setEditable(false).setSort(2) + .setType(FmsReportTypeEnum.INCOME_STATEMENT.getType()).setLevel(1)); + FmsLedgerSubjectBalanceRespVO revenueBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setSubjectCode("5001").setPeriodDebitAmount(BigDecimal.ZERO) + .setPeriodCreditAmount(new BigDecimal("1000.00")).setYearDebitAmount(BigDecimal.ZERO) + .setYearCreditAmount(new BigDecimal("3000.00")); + FmsLedgerSubjectBalanceRespVO expenseBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(102L) + .setSubjectCode("5601").setPeriodDebitAmount(new BigDecimal("200.00")) + .setPeriodCreditAmount(BigDecimal.ZERO).setYearDebitAmount(new BigDecimal("500.00")) + .setYearCreditAmount(BigDecimal.ZERO); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Arrays.asList(revenueBalance, expenseBalance)); + + // 准备参数 + FmsReportListReqVO reqVO = buildQueryReqVO(); + + // 调用 + List result = incomeStatementService.getIncomeStatement(reqVO, 10L); + + // 断言 + assertEquals(3, result.size()); + assertEquals(new BigDecimal("1000.00"), result.get(0).getCurrentAmount()); + assertEquals(new BigDecimal("500.00"), result.get(1).getYearAmount()); + assertEquals(new BigDecimal("800.00"), result.get(2).getCurrentAmount()); + assertEquals(new BigDecimal("2500.00"), result.get(2).getYearAmount()); + } + + @Test + public void testGetIncomeStatement_subjectCodeChanged() { + // mock 数据 + incomeStatementConfigMapper.insert(new FmsIncomeStatementConfigDO().setName("一、营业收入") + .setRowNo(1).setFormula("[{\"subjectId\":101,\"subjectName\":\"主营业务收入\"," + + "\"subjectNumber\":\"5001\",\"operator\":\"+\",\"rules\":6}]") + .setEditable(true).setSort(0).setAccountSetId(1L).setLevel(1)); + revenueSubject.setCode("50010"); + FmsLedgerSubjectBalanceRespVO revenueBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setSubjectCode("50010").setPeriodDebitAmount(BigDecimal.ZERO) + .setPeriodCreditAmount(new BigDecimal("1000.00")).setYearDebitAmount(BigDecimal.ZERO) + .setYearCreditAmount(new BigDecimal("3000.00")); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(revenueBalance)); + + // 调用 + List result = incomeStatementService.getIncomeStatement(buildQueryReqVO(), 10L); + + // 断言 + assertEquals(new BigDecimal("1000.00"), CollUtil.getFirst(result).getCurrentAmount()); + List formulas = JsonUtils.parseArray( + CollUtil.getFirst(result).getFormula(), FmsReportFormulaRespVO.class); + assertEquals("50010", CollUtil.getFirst(formulas).getSubjectNumber()); + } + + @Test + public void testGetIncomeStatement_formulaSnapshot() { + // mock 数据 + String revenueFormula = "[{\"subjectId\":101,\"subjectName\":\"主营业务收入\"," + + "\"subjectNumber\":\"5001\",\"operator\":\"+\",\"rules\":6}]"; + String expenseFormula = "[{\"subjectId\":102,\"subjectName\":\"销售费用\"," + + "\"subjectNumber\":\"5601\",\"operator\":\"+\",\"rules\":5}]"; + FmsIncomeStatementConfigDO config = new FmsIncomeStatementConfigDO().setName("一、营业收入") + .setRowNo(1).setFormula(revenueFormula).setEditable(true).setSort(1) + .setAccountSetId(1L).setLevel(1); + incomeStatementConfigMapper.insert(config); + FmsLedgerSubjectBalanceRespVO revenueBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setPeriodDebitAmount(BigDecimal.ZERO).setPeriodCreditAmount(new BigDecimal("1000.00")) + .setYearDebitAmount(BigDecimal.ZERO).setYearCreditAmount(new BigDecimal("3000.00")); + FmsLedgerSubjectBalanceRespVO expenseBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(102L) + .setPeriodDebitAmount(new BigDecimal("200.00")).setPeriodCreditAmount(BigDecimal.ZERO) + .setYearDebitAmount(new BigDecimal("500.00")).setYearCreditAmount(BigDecimal.ZERO); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Arrays.asList(revenueBalance, expenseBalance)); + + // 调用 + List augustResult = incomeStatementService.getIncomeStatement( + buildQueryReqVO("2026-08", "2026-08"), 10L); + incomeStatementConfigMapper.updateById(new FmsIncomeStatementConfigDO().setId(config.getId()) + .setFormula(expenseFormula)); + List augustResultAfterUpdate = incomeStatementService.getIncomeStatement( + buildQueryReqVO("2026-08", "2026-08"), 10L); + List septemberResult = incomeStatementService.getIncomeStatement( + buildQueryReqVO("2026-09", "2026-09"), 10L); + + // 断言 + assertEquals(new BigDecimal("1000.00"), CollUtil.getFirst(augustResult).getCurrentAmount()); + assertEquals(new BigDecimal("1000.00"), CollUtil.getFirst(augustResultAfterUpdate).getCurrentAmount()); + assertEquals(new BigDecimal("200.00"), CollUtil.getFirst(septemberResult).getCurrentAmount()); + assertEquals(revenueFormula, CollUtil.getFirst(incomeStatementReportMapper.selectListByPeriod( + 1L, 202608, 202608, 1)).getFormula()); + assertEquals(expenseFormula, CollUtil.getFirst(incomeStatementReportMapper.selectListByPeriod( + 1L, 202609, 202609, 1)).getFormula()); + } + + @Test + public void testUpdateIncomeStatementFormula() { + // mock 数据 + String oldFormula = "[]"; + FmsIncomeStatementConfigDO config = new FmsIncomeStatementConfigDO().setName("一、营业收入") + .setRowNo(1).setFormula(oldFormula).setEditable(true).setSort(1) + .setAccountSetId(1L).setLevel(1); + incomeStatementConfigMapper.insert(config); + FmsIncomeStatementReportDO currentReport = new FmsIncomeStatementReportDO().setName("一、营业收入") + .setRowNo(1).setFormula(oldFormula).setEditable(true).setSort(1).setAccountSetId(1L).setLevel(1) + .setFromPeriod(202608).setToPeriod(202608).setType(1) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO).setSettled(false); + incomeStatementReportMapper.insert(currentReport); + FmsIncomeStatementReportDO historyReport = new FmsIncomeStatementReportDO().setName("一、营业收入") + .setRowNo(1).setFormula(oldFormula).setEditable(true).setSort(1).setAccountSetId(1L).setLevel(1) + .setFromPeriod(202607).setToPeriod(202607).setType(1) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO).setSettled(false); + incomeStatementReportMapper.insert(historyReport); + + // 准备参数 + FmsReportFormulaUpdateReqVO.Formula formulaReqVO = new FmsReportFormulaUpdateReqVO.Formula(); + formulaReqVO.setSubjectId(revenueSubject.getId()); + formulaReqVO.setOperator("+"); + formulaReqVO.setRules(6); + FmsReportFormulaUpdateReqVO reqVO = new FmsReportFormulaUpdateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setId(currentReport.getId()); + reqVO.setFormulas(Collections.singletonList(formulaReqVO)); + + // 调用 + incomeStatementService.updateIncomeStatementFormula(reqVO, 10L); + + // 断言 + String formula = incomeStatementReportMapper.selectById(currentReport.getId()).getFormula(); + assertEquals(oldFormula, incomeStatementConfigMapper.selectById(config.getId()).getFormula()); + assertTrue(formula.contains("\"subjectId\":101")); + assertEquals(oldFormula, incomeStatementReportMapper.selectById(historyReport.getId()).getFormula()); + } + + @Test + public void testUpdateIncomeStatementFormula_ruleInvalid() { + // mock 数据 + FmsIncomeStatementConfigDO config = new FmsIncomeStatementConfigDO().setName("一、营业收入") + .setRowNo(1).setFormula("[]").setEditable(true).setSort(1).setAccountSetId(1L).setLevel(1); + incomeStatementConfigMapper.insert(config); + FmsIncomeStatementReportDO report = new FmsIncomeStatementReportDO().setName("一、营业收入") + .setRowNo(1).setFormula("[]").setEditable(true).setSort(1).setAccountSetId(1L).setLevel(1) + .setFromPeriod(202608).setToPeriod(202608).setType(1) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO).setSettled(false); + incomeStatementReportMapper.insert(report); + + // 准备参数 + FmsReportFormulaUpdateReqVO.Formula formulaReqVO = new FmsReportFormulaUpdateReqVO.Formula(); + formulaReqVO.setSubjectId(revenueSubject.getId()); + formulaReqVO.setOperator("+"); + formulaReqVO.setRules(0); + FmsReportFormulaUpdateReqVO reqVO = new FmsReportFormulaUpdateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setId(report.getId()); + reqVO.setFormulas(Collections.singletonList(formulaReqVO)); + + // 调用,并断言 + assertServiceException(() -> incomeStatementService.updateIncomeStatementFormula(reqVO, 10L), + REPORT_FORMULA_RULE_INVALID); + } + + @Test + public void testUpdateIncomeStatementFormula_notEditable() { + // mock 数据 + FmsIncomeStatementReportDO report = new FmsIncomeStatementReportDO().setName("二、营业利润") + .setRowNo(21).setFormula("[\"L1-L2\"]").setEditable(false).setSort(20) + .setAccountSetId(1L).setLevel(1).setFromPeriod(202608).setToPeriod(202608).setType(1) + .setCurrentAmount(BigDecimal.ZERO).setYearAmount(BigDecimal.ZERO).setSettled(false); + incomeStatementReportMapper.insert(report); + + // 准备参数 + FmsReportFormulaUpdateReqVO reqVO = new FmsReportFormulaUpdateReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setId(report.getId()); + reqVO.setFormulas(Collections.emptyList()); + + // 调用,并断言 + assertServiceException(() -> incomeStatementService.updateIncomeStatementFormula(reqVO, 10L), + REPORT_CONFIG_NOT_EDITABLE); + } + + @Test + public void testCheckIncomeStatement() { + // mock 数据 + FmsSubjectDO skippedSubject = new FmsSubjectDO().setId(105L).setCode("6001") + .setName("以前年度损益调整").setBalanceDirection(1).setLevel(1).setAccountSetId(1L) + .setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + revenueSubject.setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + expenseSubject.setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Arrays.asList(revenueSubject, expenseSubject, skippedSubject)); + reportTemplateMapper.insert(new FmsReportTemplateDO().setName("四、净利润").setRowNo(32) + .setFormula("[{\"operator\":\"+\",\"rules\":6,\"subjectNumber\":\"5001\"}]") + .setEditable(false).setSort(0).setType(FmsReportTypeEnum.INCOME_STATEMENT.getType()) + .setLevel(1)); + FmsLedgerSubjectBalanceRespVO revenueBalance = new FmsLedgerSubjectBalanceRespVO().setSubjectId(101L) + .setSubjectCode("5001").setPeriodDebitAmount(BigDecimal.ZERO) + .setPeriodCreditAmount(new BigDecimal("1000.00")).setYearDebitAmount(BigDecimal.ZERO) + .setYearCreditAmount(new BigDecimal("3000.00")); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Collections.singletonList(revenueBalance)); + when(balanceSheetService.getBalanceSheet(any(), eq(10L))).thenReturn(Collections.singletonList( + new FmsBalanceSheetRowRespVO().setLiabilityRowNo(51) + .setLiabilityOpeningAmount(BigDecimal.ZERO) + .setLiabilityClosingAmount(new BigDecimal("3000.00")))); + + // 调用 + FmsIncomeStatementCheckRespVO result = incomeStatementService.checkIncomeStatement( + buildQueryReqVO(), 10L); + + // 断言 + assertTrue(result.getBalanced()); + assertEquals(new BigDecimal("0.00"), result.getDifferenceAmount()); + assertEquals(1, result.getUnmappedSubjects().size()); + assertEquals(expenseSubject.getId(), CollUtil.getFirst(result.getUnmappedSubjects()).getId()); + } + + // ========== 随机对象 ========== + + private FmsReportListReqVO buildQueryReqVO() { + return buildQueryReqVO("2026-08", "2026-08"); + } + + private FmsReportListReqVO buildQueryReqVO(String startMonth, String endMonth) { + FmsReportListReqVO reqVO = new FmsReportListReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setStartMonth(startMonth); + reqVO.setEndMonth(endMonth); + return reqVO; + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonServiceImplTest.java new file mode 100644 index 0000000..c44f62d --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/report/FmsReportCommonServiceImplTest.java @@ -0,0 +1,208 @@ +package cn.iocoder.yudao.module.fms.service.report; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSubjectAmountVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingSchemeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.closing.FmsClosingVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum; +import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTypeEnum; +import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService; +import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.LEDGER_PERIOD_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import(FmsReportCommonServiceImpl.class) +public class FmsReportCommonServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsReportCommonServiceImpl reportCommonService; + + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsLedgerService ledgerService; + @MockitoBean + private FmsClosingVoucherService closingVoucherService; + @MockitoBean + private FmsClosingSchemeService closingSchemeService; + @MockitoBean + private FmsVoucherService voucherService; + + @Test + public void testGetSubjectBalanceMap_excludeProfitLossClosingVoucher() { + // mock 数据 + FmsSubjectDO profitLossRoot = new FmsSubjectDO().setId(100L).setParentId(0L) + .setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO profitLossSubject = new FmsSubjectDO().setId(101L).setParentId(100L) + .setType(FmsSubjectTypeEnum.PROFIT_LOSS.getType()); + FmsSubjectDO retainedEarningsSubject = new FmsSubjectDO().setId(201L).setParentId(0L) + .setType(FmsSubjectTypeEnum.EQUITY.getType()); + when(subjectService.getSubjectList(1L, null, 10L)).thenReturn( + Arrays.asList(profitLossRoot, profitLossSubject, retainedEarningsSubject)); + FmsLedgerSubjectBalanceRespVO profitLossBalance = buildProfitLossBalance(100L); + FmsLedgerSubjectBalanceRespVO profitLossChildBalance = buildProfitLossBalance(101L); + profitLossBalance.setChildren(Collections.singletonList(profitLossChildBalance)); + FmsLedgerSubjectBalanceRespVO retainedEarningsBalance = new FmsLedgerSubjectBalanceRespVO() + .setSubjectId(201L).setOpeningDebitAmount(BigDecimal.ZERO) + .setOpeningCreditAmount(new BigDecimal("10.00")) + .setPeriodDebitAmount(new BigDecimal("30.00")).setPeriodCreditAmount(BigDecimal.ZERO) + .setYearDebitAmount(new BigDecimal("30.00")).setYearCreditAmount(new BigDecimal("10.00")) + .setEndingDebitAmount(new BigDecimal("20.00")).setEndingCreditAmount(BigDecimal.ZERO) + .setChildren(Collections.emptyList()); + when(ledgerService.getSubjectBalanceList(any(), eq(10L))) + .thenReturn(Arrays.asList(profitLossBalance, retainedEarningsBalance)); + + LocalDateTime startTime = LocalDateTime.of(2026, 8, 1, 0, 0); + LocalDateTime endTime = LocalDateTime.of(2026, 9, 1, 0, 0); + FmsClosingVoucherDO julyClosingVoucher = new FmsClosingVoucherDO().setVoucherId(1L) + .setVoucherTime(LocalDateTime.of(2026, 7, 31, 0, 0)); + FmsClosingVoucherDO augustClosingVoucher = new FmsClosingVoucherDO().setVoucherId(2L) + .setVoucherTime(LocalDateTime.of(2026, 8, 31, 0, 0)); + when(closingSchemeService.getClosingSchemeByAccountSetIdAndType( + 1L, FmsClosingTypeEnum.PROFIT_LOSS.getType())) + .thenReturn(new FmsClosingSchemeDO().setId(11L)); + when(closingVoucherService.getClosingVoucherListByClosingIdAndPeriod(11L, null, endTime)) + .thenReturn(Arrays.asList(julyClosingVoucher, augustClosingVoucher)); + when(voucherService.getVoucherSubjectAmountList(Collections.singletonList(1L))) + .thenReturn(Arrays.asList(buildSubjectAmount(101L, "10.00", "0.00"), + buildSubjectAmount(201L, "0.00", "10.00"))); + when(voucherService.getVoucherSubjectAmountList(Collections.singletonList(2L))) + .thenReturn(Arrays.asList(buildSubjectAmount(101L, "0.00", "30.00"), + buildSubjectAmount(201L, "30.00", "0.00"))); + when(voucherService.getVoucherSubjectAmountList(Arrays.asList(1L, 2L))) + .thenReturn(Arrays.asList(buildSubjectAmount(101L, "10.00", "30.00"), + buildSubjectAmount(201L, "30.00", "10.00"))); + + // 调用 + Map result = reportCommonService.getSubjectBalanceMap( + 1L, YearMonth.of(2026, 8), YearMonth.of(2026, 8), 10L); + + // 断言 + assertProfitLossBalance(result.get(100L)); + assertProfitLossBalance(result.get(101L)); + assertEquals(new BigDecimal("10.00"), result.get(201L).getOpeningCreditAmount()); + assertEquals(new BigDecimal("30.00"), result.get(201L).getPeriodDebitAmount()); + assertEquals(new BigDecimal("20.00"), result.get(201L).getEndingDebitAmount()); + } + + @Test + public void testGetPeriodType_periodInvalid() { + // 调用,并断言异常 + assertServiceException(() -> reportCommonService.getPeriodType(202609, 202608), + LEDGER_PERIOD_INVALID); + } + + @Test + public void testCalculateBalanceAmount_subjectBalanceByFirstChild() { + // mock 数据 + FmsSubjectDO parent = new FmsSubjectDO().setId(100L).setBalanceDirection(1); + FmsSubjectDO debitChild = new FmsSubjectDO().setId(101L).setParentId(100L).setBalanceDirection(1); + FmsSubjectDO creditChild = new FmsSubjectDO().setId(102L).setParentId(100L).setBalanceDirection(1); + Map subjectMap = new LinkedHashMap<>(); + subjectMap.put(parent.getId(), parent); + subjectMap.put(debitChild.getId(), debitChild); + subjectMap.put(creditChild.getId(), creditChild); + Map balanceMap = new LinkedHashMap<>(); + balanceMap.put(parent.getId(), buildBalance(100L, "70.00", "0.00", "80.00", "0.00")); + balanceMap.put(debitChild.getId(), buildBalance(101L, "100.00", "0.00", "120.00", "0.00")); + balanceMap.put(creditChild.getId(), buildBalance(102L, "0.00", "30.00", "0.00", "40.00")); + + // 调用 + BigDecimal openingDebitAmount = reportCommonService.calculateBalanceAmount( + FmsFormulaRuleEnum.SUBJECT_DEBIT_BALANCE.getRule(), parent, balanceMap, subjectMap, true); + BigDecimal openingCreditAmount = reportCommonService.calculateBalanceAmount( + FmsFormulaRuleEnum.SUBJECT_CREDIT_BALANCE.getRule(), parent, balanceMap, subjectMap, true); + BigDecimal closingDebitAmount = reportCommonService.calculateBalanceAmount( + FmsFormulaRuleEnum.SUBJECT_DEBIT_BALANCE.getRule(), parent, balanceMap, subjectMap, false); + BigDecimal closingCreditAmount = reportCommonService.calculateBalanceAmount( + FmsFormulaRuleEnum.SUBJECT_CREDIT_BALANCE.getRule(), parent, balanceMap, subjectMap, false); + + // 断言 + assertAmountEquals("100.00", openingDebitAmount); + assertAmountEquals("30.00", openingCreditAmount); + assertAmountEquals("120.00", closingDebitAmount); + assertAmountEquals("40.00", closingCreditAmount); + } + + @Test + public void testCalculateIncomeOccurrenceAmount_netOppositeAmountForPlusOperator() { + // mock 数据 + FmsSubjectDO subject = new FmsSubjectDO().setId(101L).setBalanceDirection(2); + FmsLedgerSubjectBalanceRespVO balance = new FmsLedgerSubjectBalanceRespVO() + .setPeriodDebitAmount(new BigDecimal("20.00")).setPeriodCreditAmount(new BigDecimal("100.00")) + .setYearDebitAmount(new BigDecimal("30.00")).setYearCreditAmount(new BigDecimal("200.00")); + + // 调用,并断言 + assertAmountEquals("80.00", reportCommonService.calculateIncomeOccurrenceAmount( + FmsFormulaRuleEnum.CREDIT_AMOUNT.getRule(), subject, balance, true, "+")); + assertAmountEquals("170.00", reportCommonService.calculateIncomeOccurrenceAmount( + FmsFormulaRuleEnum.CREDIT_AMOUNT.getRule(), subject, balance, false, "+")); + assertAmountEquals("100.00", reportCommonService.calculateIncomeOccurrenceAmount( + FmsFormulaRuleEnum.CREDIT_AMOUNT.getRule(), subject, balance, true, "-")); + } + + // ========== 随机对象 ========== + + private FmsLedgerSubjectBalanceRespVO buildProfitLossBalance(Long subjectId) { + return new FmsLedgerSubjectBalanceRespVO().setSubjectId(subjectId) + .setOpeningDebitAmount(new BigDecimal("10.00")).setOpeningCreditAmount(BigDecimal.ZERO) + .setPeriodDebitAmount(new BigDecimal("7.00")).setPeriodCreditAmount(new BigDecimal("30.00")) + .setYearDebitAmount(new BigDecimal("17.00")).setYearCreditAmount(new BigDecimal("30.00")) + .setEndingDebitAmount(BigDecimal.ZERO).setEndingCreditAmount(new BigDecimal("13.00")) + .setChildren(Collections.emptyList()); + } + + private FmsLedgerSubjectBalanceRespVO buildBalance(Long subjectId, String openingDebitAmount, + String openingCreditAmount, String endingDebitAmount, String endingCreditAmount) { + return new FmsLedgerSubjectBalanceRespVO().setSubjectId(subjectId) + .setOpeningDebitAmount(new BigDecimal(openingDebitAmount)) + .setOpeningCreditAmount(new BigDecimal(openingCreditAmount)) + .setEndingDebitAmount(new BigDecimal(endingDebitAmount)) + .setEndingCreditAmount(new BigDecimal(endingCreditAmount)); + } + + private FmsVoucherSubjectAmountVO buildSubjectAmount(Long subjectId, String debitAmount, + String creditAmount) { + return new FmsVoucherSubjectAmountVO().setSubjectId(subjectId) + .setDebitAmount(new BigDecimal(debitAmount)).setCreditAmount(new BigDecimal(creditAmount)); + } + + private void assertProfitLossBalance(FmsLedgerSubjectBalanceRespVO balance) { + assertAmountEquals("0.00", balance.getOpeningDebitAmount()); + assertAmountEquals("0.00", balance.getOpeningCreditAmount()); + assertAmountEquals("7.00", balance.getPeriodDebitAmount()); + assertAmountEquals("0.00", balance.getPeriodCreditAmount()); + assertAmountEquals("7.00", balance.getYearDebitAmount()); + assertAmountEquals("0.00", balance.getYearCreditAmount()); + assertAmountEquals("7.00", balance.getEndingDebitAmount()); + assertAmountEquals("0.00", balance.getEndingCreditAmount()); + } + + private void assertAmountEquals(String expected, BigDecimal actual) { + assertEquals(0, new BigDecimal(expected).compareTo(actual)); + } + +} diff --git a/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherServiceImplTest.java b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherServiceImplTest.java new file mode 100644 index 0000000..3933135 --- /dev/null +++ b/yudao-module-fms/src/test/java/cn/iocoder/yudao/module/fms/service/voucher/FmsVoucherServiceImplTest.java @@ -0,0 +1,1173 @@ +package cn.iocoder.yudao.module.fms.service.voucher; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherEntrySaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherAttachmentUpdateReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherMoveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherPageReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSaveReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsReqVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherStatisticsRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherSubjectAmountVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherTidyReqVO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryCombinationDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.voucher.FmsVoucherEntryDO; +import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO; +import cn.iocoder.yudao.module.fms.dal.mysql.voucher.FmsVoucherEntryMapper; +import cn.iocoder.yudao.module.fms.dal.mysql.voucher.FmsVoucherMapper; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherStatusEnum; +import cn.iocoder.yudao.module.fms.enums.voucher.FmsVoucherTidyTypeEnum; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService; +import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService; +import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryCombinationService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryItemService; +import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService; +import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService; +import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportRespVO; +import cn.iocoder.yudao.module.fms.controller.admin.voucher.vo.FmsVoucherImportExcelVO; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doThrow; + +@Import(FmsVoucherServiceImpl.class) +public class FmsVoucherServiceImplTest extends BaseDbUnitTest { + + @Resource + private FmsVoucherServiceImpl voucherService; + @Resource + private FmsVoucherMapper voucherMapper; + @Resource + private FmsVoucherEntryMapper voucherEntryMapper; + + @MockitoBean + private FmsAccountSetService accountSetService; + @MockitoBean + private FmsClosingPeriodService closingPeriodService; + @MockitoBean + private FmsClosingVoucherService closingVoucherService; + @MockitoBean + private FmsVoucherWordService voucherWordService; + @MockitoBean + private FmsSubjectService subjectService; + @MockitoBean + private FmsAuxiliaryItemService auxiliaryItemService; + @MockitoBean + private FmsAuxiliaryTypeService auxiliaryTypeService; + @MockitoBean + private FmsAuxiliaryCombinationService auxiliaryCombinationService; + + private FmsSubjectDO cashSubject; + private FmsSubjectDO payableSubject; + private FmsSubjectDO cashChildSubject; + private FmsSubjectDO bankSubject; + + @BeforeEach + public void before() { + cashSubject = buildSubject(101L, "1001", "库存现金"); + payableSubject = buildSubject(201L, "2202", "应付账款"); + cashChildSubject = buildSubject(102L, "100101", "人民币现金") + .setParentId(cashSubject.getId()).setLevel(2); + bankSubject = buildSubject(103L, "1002", "银行存款"); + when(subjectService.getSubjectList(1L, null, 10L)) + .thenReturn(Arrays.asList(cashSubject, cashChildSubject, bankSubject, payableSubject)); + when(accountSetService.validateAccountSetReadPermission(1L, 10L)) + .thenReturn(new FmsAccountSetDO().setId(1L)); + when(voucherWordService.validateVoucherWordExists(1L, 11L)) + .thenReturn(new FmsVoucherWordDO().setId(11L).setAccountSetId(1L).setName("记")); + when(voucherWordService.getVoucherWordList(1L)).thenReturn(Collections.singletonList( + new FmsVoucherWordDO().setId(11L).setAccountSetId(1L).setName("记"))); + when(voucherWordService.getVoucherWordMapByName(1L)).thenReturn(Collections.singletonMap( + "记", new FmsVoucherWordDO().setId(11L).setAccountSetId(1L).setName("记"))); + when(auxiliaryTypeService.getAuxiliaryTypeList(1L, 10L)).thenReturn(Collections.emptyList()); + when(auxiliaryTypeService.validateAuxiliaryTypeList(eq(1L), anyCollection())) + .thenReturn(Collections.emptyList()); + when(auxiliaryItemService.getAuxiliaryItemListByAccountSetId(1L, 10L)) + .thenReturn(Collections.emptyList()); + } + + @Test + public void testGetVoucherCountByVoucherWordId() { + // mock 数据 + voucherMapper.insert(buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus())); + voucherMapper.insert(buildVoucher(2L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus())); + + // 调用,并断言 + assertEquals(1L, voucherService.getVoucherCountByVoucherWordId(1L, 11L)); + assertEquals(0L, voucherService.getVoucherCountByVoucherWordId(1L, 12L)); + } + + @Test + public void testGetVoucherSubjectAmountList() { + // mock 数据 + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(1L).setVoucherId(11L) + .setSubjectId(101L).setDebitAmount(new BigDecimal("10.00")) + .setCreditAmount(BigDecimal.ZERO)); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(1L).setVoucherId(12L) + .setSubjectId(101L).setDebitAmount(new BigDecimal("20.00")) + .setCreditAmount(BigDecimal.ZERO)); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(1L).setVoucherId(12L) + .setSubjectId(102L).setDebitAmount(BigDecimal.ZERO) + .setCreditAmount(new BigDecimal("30.00"))); + + // 调用 + List result = voucherService.getVoucherSubjectAmountList( + Arrays.asList(11L, 12L)); + + // 断言 + assertEquals(2, result.size()); + assertEquals(0, new BigDecimal("30.00").compareTo(result.get(0).getDebitAmount())); + assertEquals(0, new BigDecimal("30.00").compareTo(result.get(1).getCreditAmount())); + } + + @Test + public void testGetVoucherEntryCountByAuxiliary() { + // mock 数据 + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(1L) + .setAuxiliaries(Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .typeId(31L).itemId(41L).build()))); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(2L) + .setAuxiliaries(Collections.singletonList(FmsVoucherEntryDO.AuxiliaryItem.builder() + .typeId(31L).itemId(41L).build()))); + + // 调用,并断言 + assertEquals(1L, voucherService.getVoucherEntryCountByAuxiliaryItemIds( + 1L, Collections.singletonList(41L))); + assertEquals(1L, voucherService.getVoucherEntryCountByAuxiliaryTypeId(1L, 31L)); + assertEquals(0L, voucherService.getVoucherEntryCountByAuxiliaryItemIds( + 1L, Collections.singletonList(42L))); + assertEquals(0L, voucherService.getVoucherEntryCountByAuxiliaryItemIds( + 1L, Collections.emptyList())); + } + + @Test + public void testMigrateVoucherEntryAuxiliaries() { + // mock 数据 + FmsVoucherEntryDO entry = new FmsVoucherEntryDO().setAccountSetId(1L) + .setVoucherId(11L).setSubjectId(101L); + voucherEntryMapper.insert(entry); + FmsAuxiliaryCombinationDO combination = new FmsAuxiliaryCombinationDO().setId(51L) + .setItems(Collections.singletonList(FmsAuxiliaryCombinationDO.AuxiliaryItem.builder() + .type(1).typeId(31L).itemId(41L).name("北京客户").build())); + + // 调用 + voucherService.migrateVoucherEntryAuxiliaries(1L, 101L, combination); + + // 断言 + FmsVoucherEntryDO result = voucherEntryMapper.selectById(entry.getId()); + assertEquals(combination.getId(), result.getAssistCombinationId()); + assertEquals(1, result.getAuxiliaries().size()); + assertEquals(41L, result.getAuxiliaries().get(0).getItemId()); + assertEquals("北京客户", result.getAuxiliaries().get(0).getName()); + } + + @Test + public void testGetVoucherEntryQuantityCountBySubjectIds() { + // mock 数据 + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(1L) + .setSubjectId(101L).setQuantity(new BigDecimal("2.00"))); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(1L) + .setSubjectId(101L).setQuantity(BigDecimal.ZERO)); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setAccountSetId(1L) + .setSubjectId(102L).setQuantity(new BigDecimal("3.00"))); + + // 调用,并断言 + assertEquals(1L, voucherService.getVoucherEntryQuantityCountBySubjectIds( + 1L, Collections.singletonList(101L))); + assertEquals(2L, voucherService.getVoucherEntryQuantityCountBySubjectIds( + 1L, Arrays.asList(101L, 102L))); + } + + @Test + public void testCreateVoucher_success() { + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + reqVO.getEntries().get(0).setId(90001L); + reqVO.getEntries().get(1).setId(90002L); + reqVO.setVoucherTime(LocalDateTime.of(2026, 8, 2, 15, 30)); + reqVO.setAttachmentCount(3); + + // 调用 + Long voucherId = voucherService.createVoucher(reqVO, 10L); + + // 断言 + FmsVoucherDO voucher = voucherMapper.selectById(voucherId); + assertEquals(1, voucher.getVoucherNumber()); + assertEquals(LocalDateTime.of(2026, 8, 2, 0, 0), voucher.getVoucherTime()); + assertEquals(new BigDecimal("100.00"), voucher.getDebitAmount()); + assertEquals(new BigDecimal("100.00"), voucher.getCreditAmount()); + assertEquals(3, voucher.getAttachmentCount()); + assertEquals(FmsVoucherStatusEnum.PENDING_REVIEW.getStatus(), voucher.getStatus()); + List entries = voucherEntryMapper.selectListByVoucherIds( + Collections.singletonList(voucherId)); + assertEquals(2, entries.size()); + assertFalse(entries.stream().anyMatch( + entry -> Arrays.asList(90001L, 90002L).contains(entry.getId()))); + } + + @Test + public void testCreateVoucher_negativeAmount() { + // 准备参数:红字分录保留在原借贷方向 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("-20.00", "-20.00"); + + // 调用 + Long voucherId = voucherService.createVoucher(reqVO, 10L); + + // 断言 + FmsVoucherDO voucher = voucherMapper.selectById(voucherId); + assertEquals(new BigDecimal("-20.00"), voucher.getDebitAmount()); + assertEquals(new BigDecimal("-20.00"), voucher.getCreditAmount()); + } + + @Test + public void testCreateVoucher_quantityAmountFloor() { + // mock 数据 + bankSubject.setQuantityAccounting(true); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("1.99", "1.99"); + CollUtil.getFirst(reqVO.getEntries()).setQuantity(new BigDecimal("1")); + CollUtil.getFirst(reqVO.getEntries()).setUnitPrice(new BigDecimal("1.999")); + + // 调用 + Long voucherId = voucherService.createVoucher(reqVO, 10L); + + // 断言 + FmsVoucherEntryDO entry = CollUtil.getFirst(voucherEntryMapper.selectListByVoucherIds( + Collections.singletonList(voucherId))); + assertEquals(new BigDecimal("1.99"), entry.getDebitAmount()); + assertEquals(0, new BigDecimal("1").compareTo(entry.getQuantity())); + assertEquals(0, new BigDecimal("1.999").compareTo(entry.getUnitPrice())); + } + + @Test + public void testCreateVoucher_attachmentUrlsEmpty() { + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + + // 调用 + Long voucherId = voucherService.createVoucher(reqVO, 10L); + + // 断言 + FmsVoucherDO voucher = voucherMapper.selectById(voucherId); + assertEquals(0, voucher.getAttachmentCount()); + assertEquals(Collections.emptyList(), voucher.getAttachmentUrls()); + } + + @Test + public void testCreateVoucher_numberDuplicateInSameMonth() { + // mock 数据 + voucherMapper.insert(buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus())); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + reqVO.setVoucherNumber(1); + reqVO.setVoucherTime(LocalDateTime.of(2026, 8, 3, 0, 0)); + + // 调用,并断言 + assertServiceException(() -> voucherService.createVoucher(reqVO, 10L), + VOUCHER_NUMBER_DUPLICATE); + } + + @Test + public void testCreateVoucher_sameNumberInNextMonth() { + // mock 数据 + voucherMapper.insert(buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus())); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + reqVO.setVoucherNumber(1); + reqVO.setVoucherTime(LocalDateTime.of(2026, 9, 1, 0, 0)); + + // 调用 + Long voucherId = voucherService.createVoucher(reqVO, 10L); + + // 断言 + assertEquals(1, voucherMapper.selectById(voucherId).getVoucherNumber()); + } + + @Test + public void testCreateVoucher_periodClosed() { + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + doThrow(exception(CLOSING_PERIOD_CLOSED)).when(closingPeriodService) + .validatePeriodOpen(1L, reqVO.getVoucherTime()); + + // 调用,并断言 + assertServiceException(() -> voucherService.createVoucher(reqVO, 10L), + CLOSING_PERIOD_CLOSED); + } + + @Test + public void testCreateVoucher_unbalanced() { + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "90.00"); + + // 调用,并断言 + assertServiceException(() -> voucherService.createVoucher(reqVO, 10L), + VOUCHER_AMOUNT_UNBALANCED); + } + + @Test + public void testCreateVoucher_auxiliarySnapshot() { + // mock 数据 + bankSubject.setAuxiliaryTypeIds(Collections.singletonList(31L)); + FmsAuxiliaryTypeDO auxiliaryType = new FmsAuxiliaryTypeDO().setId(31L) + .setAccountSetId(1L).setName("客户").setType(1); + FmsAuxiliaryItemDO auxiliaryItem = new FmsAuxiliaryItemDO().setId(41L) + .setAccountSetId(1L).setAuxiliaryTypeId(31L).setCode("KH001") + .setName("上海客户").setStatus(CommonStatusEnum.ENABLE.getStatus()); + when(auxiliaryItemService.validateAuxiliaryItemList(eq(1L), anyCollection())) + .thenReturn(Collections.singletonList(auxiliaryItem)); + when(auxiliaryTypeService.validateAuxiliaryTypeList(eq(1L), anyCollection())) + .thenReturn(Collections.singletonList(auxiliaryType)); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + FmsVoucherEntrySaveReqVO.AuxiliaryItem requestItem = new FmsVoucherEntrySaveReqVO.AuxiliaryItem(); + requestItem.setTypeId(31L); + requestItem.setItemId(41L); + CollUtil.getFirst(reqVO.getEntries()).setAuxiliaries(Collections.singletonList(requestItem)); + + // 调用 + Long voucherId = voucherService.createVoucher(reqVO, 10L); + + // 断言 + FmsVoucherEntryDO entry = CollUtil.getFirst(voucherEntryMapper + .selectListByVoucherIds(Collections.singletonList(voucherId))); + assertEquals("上海客户", CollUtil.getFirst(entry.getAuxiliaries()).getName()); + assertEquals(31L, CollUtil.getFirst(entry.getAuxiliaries()).getTypeId()); + } + + @Test + public void testImportVoucher_success() { + // 准备参数 + List rows = Arrays.asList( + buildImportRow(2, "1002", "采购材料", "100.00", null), + buildImportRow(3, "2202", "采购材料", null, "100.00")); + + // 调用 + FmsVoucherImportRespVO result = voucherService.importVoucher(1L, rows, 10L); + + // 断言 + assertEquals(1, result.getSuccessVoucherCount()); + assertEquals(2, result.getSuccessRowCount()); + assertEquals(0, result.getFailureVoucherCount()); + FmsVoucherDO voucher = CollUtil.getFirst(voucherMapper.selectList()); + assertEquals(2, voucher.getAttachmentCount()); + assertEquals(2, voucherEntryMapper.selectListByVoucherIds( + Collections.singletonList(voucher.getId())).size()); + } + + @Test + public void testImportVoucher_negativeAmount() { + // 准备参数:导入红字凭证时借贷方向保持不变 + List rows = Arrays.asList( + buildImportRow(2, "1002", "红字冲销", "-20.00", null), + buildImportRow(3, "2202", "红字冲销", null, "-20.00")); + + // 调用 + FmsVoucherImportRespVO result = voucherService.importVoucher(1L, rows, 10L); + + // 断言 + assertEquals(1, result.getSuccessVoucherCount()); + assertEquals(0, result.getFailureVoucherCount()); + FmsVoucherDO voucher = CollUtil.getFirst(voucherMapper.selectList()); + assertEquals(new BigDecimal("-20.00"), voucher.getDebitAmount()); + assertEquals(new BigDecimal("-20.00"), voucher.getCreditAmount()); + } + + @Test + public void testImportVoucher_quantityAmountFloor() { + // mock 数据 + bankSubject.setQuantityAccounting(true); + // 准备参数 + FmsVoucherImportExcelVO debitRow = buildImportRow(2, "1002", "采购材料", "1.99", null) + .setQuantity(new BigDecimal("1")).setUnitPrice(new BigDecimal("1.999")); + List rows = Arrays.asList(debitRow, + buildImportRow(3, "2202", "采购材料", null, "1.99")); + + // 调用 + FmsVoucherImportRespVO result = voucherService.importVoucher(1L, rows, 10L); + + // 断言 + assertEquals(1, result.getSuccessVoucherCount()); + assertEquals(0, result.getFailureVoucherCount()); + } + + @Test + public void testImportVoucher_partialSuccess() { + // mock 数据 + voucherMapper.insert(buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(2).setVoucherTime(LocalDateTime.of(2026, 8, 1, 0, 0))); + // 准备参数 + List rows = Arrays.asList( + buildImportRow(2, "1002", "有效凭证", "100.00", null).setVoucherNumber(1), + buildImportRow(3, "2202", "有效凭证", null, "100.00").setVoucherNumber(1), + buildImportRow(4, "1002", "重复凭证", "80.00", null).setVoucherNumber(2), + buildImportRow(5, "2202", "重复凭证", null, "80.00").setVoucherNumber(2)); + + // 调用 + FmsVoucherImportRespVO result = voucherService.importVoucher(1L, rows, 10L); + + // 断言 + assertEquals(1, result.getSuccessVoucherCount()); + assertEquals(1, result.getFailureVoucherCount()); + assertEquals(2, result.getFailureRowCount()); + assertTrue(result.getErrorRows().stream().allMatch(row -> + row.getErrors().contains("同一会计期间、凭证字和凭证号已存在"))); + } + + @Test + public void testImportVoucher_duplicateInSameMonth() { + // 准备参数 + List rows = Arrays.asList( + buildImportRow(2, "1002", "月初凭证", "100.00", null), + buildImportRow(3, "2202", "月初凭证", null, "100.00"), + buildImportRow(4, "1002", "月末凭证", "80.00", null) + .setVoucherTime(LocalDateTime.of(2026, 8, 31, 0, 0)), + buildImportRow(5, "2202", "月末凭证", null, "80.00") + .setVoucherTime(LocalDateTime.of(2026, 8, 31, 0, 0))); + + // 调用 + FmsVoucherImportRespVO result = voucherService.importVoucher(1L, rows, 10L); + + // 断言 + assertEquals(1, result.getSuccessVoucherCount()); + assertEquals(1, result.getFailureVoucherCount()); + assertEquals(2, result.getFailureRowCount()); + assertTrue(result.getErrorRows().stream().allMatch(row -> + row.getErrors().contains("同一会计期间、凭证字和凭证号重复"))); + } + + @Test + public void testImportVoucher_auxiliary() { + // mock 数据 + bankSubject.setAuxiliaryTypeIds(Collections.singletonList(31L)); + FmsAuxiliaryTypeDO auxiliaryType = new FmsAuxiliaryTypeDO().setId(31L) + .setAccountSetId(1L).setName("客户").setType(1); + FmsAuxiliaryItemDO auxiliaryItem = new FmsAuxiliaryItemDO().setId(41L) + .setAccountSetId(1L).setAuxiliaryTypeId(31L).setCode("KH001") + .setName("上海客户").setStatus(CommonStatusEnum.ENABLE.getStatus()); + when(auxiliaryTypeService.getAuxiliaryTypeList(1L, 10L)) + .thenReturn(Collections.singletonList(auxiliaryType)); + when(auxiliaryItemService.getAuxiliaryItemListByAccountSetId(1L, 10L)) + .thenReturn(Collections.singletonList(auxiliaryItem)); + when(auxiliaryTypeService.getAuxiliaryTypeMap(1L, 10L)) + .thenReturn(Collections.singletonMap(31L, auxiliaryType)); + when(auxiliaryItemService.getAuxiliaryItemMapByTypeIdAndCode(1L, 10L)) + .thenReturn(Collections.singletonMap("31|KH001", auxiliaryItem)); + // 准备参数 + FmsVoucherImportExcelVO debitRow = buildImportRow(2, "1002", "采购材料", "100.00", null); + debitRow.getAuxiliaryCodes().put(31L, "KH001"); + + // 调用 + FmsVoucherImportRespVO result = voucherService.importVoucher(1L, Arrays.asList( + debitRow, buildImportRow(3, "2202", "采购材料", null, "100.00")), 10L); + + // 断言 + assertEquals(1, result.getSuccessVoucherCount()); + FmsVoucherDO voucher = CollUtil.getFirst(voucherMapper.selectList()); + FmsVoucherEntryDO entry = CollUtil.getFirst(voucherEntryMapper.selectListByVoucherIds( + Collections.singletonList(voucher.getId()))); + assertEquals("上海客户", CollUtil.getFirst(entry.getAuxiliaries()).getName()); + } + + @Test + public void testDeleteVoucherList_approved() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.APPROVED.getStatus()); + voucherMapper.insert(voucher); + + // 调用,并断言 + assertServiceException(() -> voucherService.deleteVoucherList( + 1L, Collections.singletonList(voucher.getId()), 10L), VOUCHER_APPROVED_NOT_EDITABLE); + } + + @Test + public void testDeleteVoucherList_reusedVoucherNumber() { + // mock 数据 + FmsVoucherDO firstVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(firstVoucher); + voucherService.deleteVoucherList(1L, Collections.singletonList(firstVoucher.getId()), 10L); + FmsVoucherDO secondVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(secondVoucher); + + // 调用 + voucherService.deleteVoucherList(1L, Collections.singletonList(secondVoucher.getId()), 10L); + + // 断言 + assertNull(voucherMapper.selectById(secondVoucher.getId())); + } + + @Test + public void testUpdateVoucher_closingGenerated() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(voucher); + when(closingVoucherService.getClosingVoucherIdSet( + 1L, Collections.singletonList(voucher.getId()))) + .thenReturn(Collections.singleton(voucher.getId())); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + reqVO.setId(voucher.getId()); + + // 调用,并断言 + assertServiceException(() -> voucherService.updateVoucher(reqVO, 10L), + VOUCHER_CLOSING_GENERATED_NOT_EDITABLE); + } + + @Test + public void testUpdateVoucher_attachmentCount() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setAttachmentUrls(Collections.singletonList("/admin-api/infra/file/4/get")) + .setAttachmentCount(1); + voucherMapper.insert(voucher); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + reqVO.setId(voucher.getId()); + reqVO.setAttachmentCount(3); + + // 调用 + voucherService.updateVoucher(reqVO, 10L); + + // 断言 + FmsVoucherDO actual = voucherMapper.selectById(voucher.getId()); + assertEquals(3, actual.getAttachmentCount()); + assertEquals(Collections.singletonList("/admin-api/infra/file/4/get"), actual.getAttachmentUrls()); + } + + @Test + public void testUpdateVoucher_attachmentCountZero() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setAttachmentUrls(Collections.singletonList("/admin-api/infra/file/4/get")) + .setAttachmentCount(1); + voucherMapper.insert(voucher); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + reqVO.setId(voucher.getId()); + + // 调用 + voucherService.updateVoucher(reqVO, 10L); + + // 断言 + FmsVoucherDO actual = voucherMapper.selectById(voucher.getId()); + assertEquals(0, actual.getAttachmentCount()); + assertEquals(Collections.singletonList("/admin-api/infra/file/4/get"), actual.getAttachmentUrls()); + } + + @Test + public void testUpdateVoucher_diffEntries() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(voucher); + FmsVoucherEntryDO debitEntry = new FmsVoucherEntryDO().setAccountSetId(1L).setVoucherId(voucher.getId()) + .setDigest("原借方摘要").setSubjectId(101L).setSubjectCode("1001").setSubjectName("库存现金") + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setDebitAmount(new BigDecimal("100.00")).setCreditAmount(BigDecimal.ZERO) + .setSort(1).setAuxiliaries(Collections.emptyList()); + FmsVoucherEntryDO creditEntry = new FmsVoucherEntryDO().setAccountSetId(1L).setVoucherId(voucher.getId()) + .setDigest("原贷方摘要").setSubjectId(201L).setSubjectCode("2202").setSubjectName("应付账款") + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setDebitAmount(BigDecimal.ZERO).setCreditAmount(new BigDecimal("100.00")) + .setSort(2).setAuxiliaries(Collections.emptyList()); + voucherEntryMapper.insertBatch(Arrays.asList(debitEntry, creditEntry)); + // 准备参数 + FmsVoucherSaveReqVO reqVO = buildSaveReqVO("100.00", "100.00"); + reqVO.setId(voucher.getId()); + reqVO.getEntries().get(0).setId(debitEntry.getId()); + reqVO.getEntries().get(0).setDigest("新借方摘要"); + reqVO.getEntries().get(1).setId(creditEntry.getId()); + + // 调用 + voucherService.updateVoucher(reqVO, 10L); + + // 断言 + List entries = voucherEntryMapper.selectListByVoucherIds( + Collections.singletonList(voucher.getId())); + assertEquals(Arrays.asList(debitEntry.getId(), creditEntry.getId()), + Arrays.asList(entries.get(0).getId(), entries.get(1).getId())); + assertEquals("新借方摘要", entries.get(0).getDigest()); + } + + @Test + public void testUpdateVoucherAttachments_success() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setAttachmentCount(3); + voucherMapper.insert(voucher); + // 准备参数 + FmsVoucherAttachmentUpdateReqVO reqVO = new FmsVoucherAttachmentUpdateReqVO(); + reqVO.setId(voucher.getId()); + reqVO.setAccountSetId(1L); + reqVO.setAttachmentUrls(Arrays.asList( + "/admin-api/infra/file/4/get", "/admin-api/infra/file/5/get")); + + // 调用 + voucherService.updateVoucherAttachments(reqVO, 10L); + + // 断言 + FmsVoucherDO actual = voucherMapper.selectById(voucher.getId()); + assertEquals(Arrays.asList("/admin-api/infra/file/4/get", "/admin-api/infra/file/5/get"), + actual.getAttachmentUrls()); + assertEquals(3, actual.getAttachmentCount()); + } + + @Test + public void testUpdateVoucherAttachments_accountSetAccessDenied() { + // 准备参数 + FmsVoucherAttachmentUpdateReqVO reqVO = new FmsVoucherAttachmentUpdateReqVO(); + reqVO.setId(1L); + reqVO.setAccountSetId(1L); + reqVO.setAttachmentUrls(Collections.singletonList("/admin-api/infra/file/4/get")); + // mock 方法 + doThrow(exception(ACCOUNT_SET_ACCESS_DENIED)).when(accountSetService) + .validateAccountSetWritePermission(1L, 10L); + + // 调用,并断言 + assertServiceException(() -> voucherService.updateVoucherAttachments(reqVO, 10L), + ACCOUNT_SET_ACCESS_DENIED); + } + + @Test + public void testUpdateVoucherAttachments_approved() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.APPROVED.getStatus()); + voucherMapper.insert(voucher); + // 准备参数 + FmsVoucherAttachmentUpdateReqVO reqVO = new FmsVoucherAttachmentUpdateReqVO(); + reqVO.setId(voucher.getId()); + reqVO.setAccountSetId(1L); + reqVO.setAttachmentUrls(Collections.singletonList("/admin-api/infra/file/4/get")); + + // 调用,并断言 + assertServiceException(() -> voucherService.updateVoucherAttachments(reqVO, 10L), + VOUCHER_APPROVED_NOT_EDITABLE); + } + + @Test + public void testDeleteVoucherList_closingGenerated() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(voucher); + when(closingVoucherService.getClosingVoucherIdSet( + 1L, Collections.singletonList(voucher.getId()))) + .thenReturn(Collections.singleton(voucher.getId())); + + // 调用,并断言 + assertServiceException(() -> voucherService.deleteVoucherList( + 1L, Collections.singletonList(voucher.getId()), 10L), + VOUCHER_CLOSING_GENERATED_NOT_EDITABLE); + } + + @Test + public void testUpdateVoucherReviewStatus_closingGenerated() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(voucher); + when(closingVoucherService.getClosingVoucherIdSet( + 1L, Collections.singletonList(voucher.getId()))) + .thenReturn(Collections.singleton(voucher.getId())); + + // 调用,并断言 + assertServiceException(() -> voucherService.updateVoucherReviewStatus( + 1L, Collections.singletonList(voucher.getId()), + FmsVoucherStatusEnum.APPROVED.getStatus(), 10L), + VOUCHER_CLOSING_GENERATED_NOT_EDITABLE); + } + + @Test + public void testUpdateVoucherReviewStatus_success() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(voucher); + + // 调用,并断言 + voucherService.updateVoucherReviewStatus(1L, Collections.singletonList(voucher.getId()), + FmsVoucherStatusEnum.APPROVED.getStatus(), 10L); + assertEquals(10L, voucherMapper.selectById(voucher.getId()).getReviewerUserId()); + voucherService.updateVoucherReviewStatus(1L, Collections.singletonList(voucher.getId()), + FmsVoucherStatusEnum.PENDING_REVIEW.getStatus(), 10L); + assertNull(voucherMapper.selectById(voucher.getId()).getReviewerUserId()); + } + + @Test + public void testTidyVoucher_fillGaps() { + // mock 数据 + FmsVoucherDO first = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1).setVoucherTime(LocalDateTime.of(2026, 8, 1, 0, 0)); + FmsVoucherDO second = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(3).setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)); + FmsVoucherDO third = buildVoucher(1L, FmsVoucherStatusEnum.APPROVED.getStatus()) + .setVoucherNumber(5).setVoucherTime(LocalDateTime.of(2026, 8, 3, 0, 0)) + .setReviewerUserId(99L); + voucherMapper.insert(first); + voucherMapper.insert(second); + voucherMapper.insert(third); + // 准备参数 + FmsVoucherTidyReqVO reqVO = buildTidyReqVO( + 11L, 2, FmsVoucherTidyTypeEnum.FILL_GAPS.getType()); + + // 调用 + voucherService.tidyVoucher(reqVO, 10L); + + // 断言 + assertEquals(1, voucherMapper.selectById(first.getId()).getVoucherNumber()); + assertEquals(2, voucherMapper.selectById(second.getId()).getVoucherNumber()); + assertEquals(3, voucherMapper.selectById(third.getId()).getVoucherNumber()); + assertEquals(99L, voucherMapper.selectById(third.getId()).getReviewerUserId()); + } + + @Test + public void testTidyVoucher_reorderByTime() { + // mock 数据 + FmsVoucherDO first = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1).setVoucherTime(LocalDateTime.of(2026, 8, 3, 0, 0)); + FmsVoucherDO second = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(2).setVoucherTime(LocalDateTime.of(2026, 8, 1, 0, 0)); + FmsVoucherDO third = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(3).setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)); + voucherMapper.insert(first); + voucherMapper.insert(second); + voucherMapper.insert(third); + // 准备参数 + FmsVoucherTidyReqVO reqVO = buildTidyReqVO( + 11L, 1, FmsVoucherTidyTypeEnum.REORDER_BY_TIME.getType()); + + // 调用 + voucherService.tidyVoucher(reqVO, 10L); + + // 断言 + assertEquals(3, voucherMapper.selectById(first.getId()).getVoucherNumber()); + assertEquals(1, voucherMapper.selectById(second.getId()).getVoucherNumber()); + assertEquals(2, voucherMapper.selectById(third.getId()).getVoucherNumber()); + } + + @Test + public void testTidyVoucher_allWordsIndependent() { + // mock 数据 + FmsVoucherDO firstWordVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherWordId(11L).setVoucherNumber(4); + FmsVoucherDO secondWordVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherWordId(12L).setVoucherNumber(7); + voucherMapper.insert(firstWordVoucher); + voucherMapper.insert(secondWordVoucher); + // 准备参数 + FmsVoucherTidyReqVO reqVO = buildTidyReqVO( + null, 2, FmsVoucherTidyTypeEnum.FILL_GAPS.getType()); + + // 调用 + voucherService.tidyVoucher(reqVO, 10L); + + // 断言 + assertEquals(2, voucherMapper.selectById(firstWordVoucher.getId()).getVoucherNumber()); + assertEquals(2, voucherMapper.selectById(secondWordVoucher.getId()).getVoucherNumber()); + } + + @Test + public void testTidyVoucher_periodClosed() { + // 准备参数 + FmsVoucherTidyReqVO reqVO = buildTidyReqVO( + 11L, 1, FmsVoucherTidyTypeEnum.FILL_GAPS.getType()); + doThrow(exception(CLOSING_PERIOD_CLOSED)).when(closingPeriodService) + .validatePeriodOpen(1L, LocalDateTime.of(2026, 8, 1, 0, 0)); + + // 调用,并断言 + assertServiceException(() -> voucherService.tidyVoucher(reqVO, 10L), + CLOSING_PERIOD_CLOSED); + } + + @Test + public void testMoveVoucher_success() { + // mock 数据 + FmsVoucherDO first = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1); + FmsVoucherDO second = buildVoucher(1L, FmsVoucherStatusEnum.APPROVED.getStatus()) + .setVoucherNumber(2).setReviewerUserId(99L); + FmsVoucherDO third = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(3); + FmsVoucherDO fourth = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(4); + FmsVoucherDO fifth = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(5); + voucherMapper.insert(first); + voucherMapper.insert(second); + voucherMapper.insert(third); + voucherMapper.insert(fourth); + voucherMapper.insert(fifth); + // 准备参数 + FmsVoucherMoveReqVO reqVO = buildMoveReqVO(5, 2); + + // 调用 + voucherService.moveVoucher(reqVO, 10L); + + // 断言 + assertEquals(1, voucherMapper.selectById(first.getId()).getVoucherNumber()); + assertEquals(3, voucherMapper.selectById(second.getId()).getVoucherNumber()); + assertEquals(99L, voucherMapper.selectById(second.getId()).getReviewerUserId()); + assertEquals(4, voucherMapper.selectById(third.getId()).getVoucherNumber()); + assertEquals(5, voucherMapper.selectById(fourth.getId()).getVoucherNumber()); + assertEquals(2, voucherMapper.selectById(fifth.getId()).getVoucherNumber()); + } + + @Test + public void testMoveVoucher_rangeInvalid() { + // 准备参数 + FmsVoucherMoveReqVO reqVO = buildMoveReqVO(2, 2); + + // 调用,并断言 + assertServiceException(() -> voucherService.moveVoucher(reqVO, 10L), + VOUCHER_MOVE_RANGE_INVALID); + } + + @Test + public void testMoveVoucher_sourceNotExists() { + // mock 数据 + voucherMapper.insert(buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(2)); + // 准备参数 + FmsVoucherMoveReqVO reqVO = buildMoveReqVO(5, 2); + + // 调用,并断言 + assertServiceException(() -> voucherService.moveVoucher(reqVO, 10L), + VOUCHER_MOVE_SOURCE_NOT_EXISTS); + } + + @Test + public void testMoveVoucher_periodClosed() { + // 准备参数 + FmsVoucherMoveReqVO reqVO = buildMoveReqVO(5, 2); + doThrow(exception(CLOSING_PERIOD_CLOSED)).when(closingPeriodService) + .validatePeriodOpen(1L, LocalDateTime.of(2026, 8, 1, 0, 0)); + + // 调用,并断言 + assertServiceException(() -> voucherService.moveVoucher(reqVO, 10L), + CLOSING_PERIOD_CLOSED); + } + + @Test + public void testGetVoucherPage_digest() { + // mock 数据 + FmsVoucherDO first = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + first.setVoucherNumber(1); + voucherMapper.insert(first); + FmsVoucherDO second = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + second.setVoucherNumber(2); + voucherMapper.insert(second); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(first.getId()).setAccountSetId(1L) + .setDigest("购买办公用品").setSubjectId(101L).setSubjectCode("1001") + .setSubjectName("库存现金").setSort(1).setQuantity(BigDecimal.ZERO) + .setUnitPrice(BigDecimal.ZERO).setDebitAmount(new BigDecimal("100.00")) + .setCreditAmount(BigDecimal.ZERO).setAuxiliaries(Collections.emptyList())); + // 准备参数 + FmsVoucherPageReqVO reqVO = new FmsVoucherPageReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setDigest("办公"); + + // 调用 + PageResult pageResult = voucherService.getVoucherPage(reqVO, 10L); + + // 断言 + assertEquals(1L, pageResult.getTotal()); + assertEquals(first.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetVoucherPage_parentSubject() { + // mock 数据 + FmsVoucherDO childVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1); + voucherMapper.insert(childVoucher); + FmsVoucherDO otherVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(2); + voucherMapper.insert(otherVoucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(childVoucher.getId()).setAccountSetId(1L) + .setDigest("子科目分录").setSubjectId(101L).setSubjectCode("100101") + .setSubjectName("人民币现金").setSort(1).setQuantity(BigDecimal.ZERO) + .setUnitPrice(BigDecimal.ZERO).setDebitAmount(new BigDecimal("100.00")) + .setCreditAmount(BigDecimal.ZERO).setAuxiliaries(Collections.emptyList())); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(otherVoucher.getId()).setAccountSetId(1L) + .setDigest("其他科目分录").setSubjectId(201L).setSubjectCode("2202") + .setSubjectName("应付账款").setSort(1).setQuantity(BigDecimal.ZERO) + .setUnitPrice(BigDecimal.ZERO).setDebitAmount(new BigDecimal("100.00")) + .setCreditAmount(BigDecimal.ZERO).setAuxiliaries(Collections.emptyList())); + when(subjectService.getSubjectIdListWithChildren(1L, 100L)) + .thenReturn(Arrays.asList(100L, 101L)); + // 准备参数 + FmsVoucherPageReqVO reqVO = new FmsVoucherPageReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setSubjectId(100L); + + // 调用 + PageResult pageResult = voucherService.getVoucherPage(reqVO, 10L); + + // 断言 + assertEquals(1L, pageResult.getTotal()); + assertEquals(childVoucher.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetVoucherPage_entryAmount() { + // mock 数据 + FmsVoucherDO matchingVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1).setDebitAmount(new BigDecimal("250.00")) + .setCreditAmount(new BigDecimal("250.00")).setTotal(new BigDecimal("250.00")); + voucherMapper.insert(matchingVoucher); + FmsVoucherDO otherVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(2); + voucherMapper.insert(otherVoucher); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(matchingVoucher.getId()).setAccountSetId(1L) + .setDigest("匹配分录").setSubjectId(101L).setSubjectCode("1001") + .setSubjectName("库存现金").setSort(1).setQuantity(BigDecimal.ZERO) + .setUnitPrice(BigDecimal.ZERO).setDebitAmount(new BigDecimal("100.00")) + .setCreditAmount(BigDecimal.ZERO).setAuxiliaries(Collections.emptyList())); + voucherEntryMapper.insert(new FmsVoucherEntryDO().setVoucherId(otherVoucher.getId()).setAccountSetId(1L) + .setDigest("不匹配分录").setSubjectId(101L).setSubjectCode("1001") + .setSubjectName("库存现金").setSort(1).setQuantity(BigDecimal.ZERO) + .setUnitPrice(BigDecimal.ZERO).setDebitAmount(new BigDecimal("300.00")) + .setCreditAmount(BigDecimal.ZERO).setAuxiliaries(Collections.emptyList())); + // 准备参数 + FmsVoucherPageReqVO reqVO = new FmsVoucherPageReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMinAmount(new BigDecimal("90.00")); + reqVO.setMaxAmount(new BigDecimal("110.00")); + + // 调用 + PageResult pageResult = voucherService.getVoucherPage(reqVO, 10L); + + // 断言 + assertEquals(1L, pageResult.getTotal()); + assertEquals(matchingVoucher.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetVoucherPage_ids() { + // mock 数据 + FmsVoucherDO first = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1); + FmsVoucherDO second = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(2); + voucherMapper.insert(first); + voucherMapper.insert(second); + // 准备参数 + FmsVoucherPageReqVO reqVO = new FmsVoucherPageReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setIds(Collections.singletonList(second.getId())); + + // 调用 + PageResult pageResult = voucherService.getVoucherPage(reqVO, 10L); + + // 断言 + assertEquals(1L, pageResult.getTotal()); + assertEquals(second.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetVoucherPage_orderByTimeAndNumberAsc() { + // mock 数据 + FmsVoucherDO laterVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1).setVoucherTime(LocalDateTime.of(2026, 8, 3, 0, 0)); + FmsVoucherDO secondVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(2).setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)); + FmsVoucherDO firstVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherNumber(1).setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)); + voucherMapper.insert(laterVoucher); + voucherMapper.insert(secondVoucher); + voucherMapper.insert(firstVoucher); + // 准备参数 + FmsVoucherPageReqVO reqVO = new FmsVoucherPageReqVO().setAccountSetId(1L); + + // 调用 + PageResult pageResult = voucherService.getVoucherPage(reqVO, 10L); + + // 断言 + assertEquals(Arrays.asList(firstVoucher.getId(), secondVoucher.getId(), laterVoucher.getId()), + pageResult.getList().stream().map(FmsVoucherDO::getId).collect(Collectors.toList())); + } + + @Test + public void testGetVoucherStatisticsList_parentIncludesChild() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherWordId(11L).setVoucherNumber(1); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(buildVoucherEntry(voucher.getId(), cashChildSubject, + new BigDecimal("80.00"), BigDecimal.ZERO)); + voucherEntryMapper.insert(buildVoucherEntry(voucher.getId(), bankSubject, + BigDecimal.ZERO, new BigDecimal("80.00"))); + FmsVoucherDO ignoredVoucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()) + .setVoucherWordId(12L).setVoucherNumber(2) + .setVoucherTime(LocalDateTime.of(2026, 9, 2, 0, 0)); + voucherMapper.insert(ignoredVoucher); + voucherEntryMapper.insert(buildVoucherEntry(ignoredVoucher.getId(), cashChildSubject, + new BigDecimal("20.00"), BigDecimal.ZERO)); + // 准备参数 + FmsVoucherStatisticsReqVO reqVO = buildStatisticsReqVO(); + reqVO.setVoucherWordId(11L).setMinVoucherNumber(1).setMaxVoucherNumber(1); + + // 调用 + List result = voucherService.getVoucherStatisticsList(reqVO, 10L); + + // 断言 + assertEquals(2, result.size()); + assertEquals(cashSubject.getId(), result.get(0).getSubjectId()); + assertEquals(new BigDecimal("80.00"), result.get(0).getDebitAmount()); + assertEquals(bankSubject.getId(), result.get(1).getSubjectId()); + assertEquals(new BigDecimal("80.00"), result.get(1).getCreditAmount()); + } + + @Test + public void testGetVoucherStatisticsList_filterLevel() { + // mock 数据 + FmsVoucherDO voucher = buildVoucher(1L, FmsVoucherStatusEnum.PENDING_REVIEW.getStatus()); + voucherMapper.insert(voucher); + voucherEntryMapper.insert(buildVoucherEntry(voucher.getId(), cashChildSubject, + new BigDecimal("80.00"), BigDecimal.ZERO)); + // 准备参数 + FmsVoucherStatisticsReqVO reqVO = buildStatisticsReqVO(); + reqVO.setMinLevel(2).setMaxLevel(2); + + // 调用 + List result = voucherService.getVoucherStatisticsList(reqVO, 10L); + + // 断言 + assertEquals(1, result.size()); + assertEquals(cashChildSubject.getId(), result.get(0).getSubjectId()); + assertEquals(new BigDecimal("80.00"), result.get(0).getDebitAmount()); + } + + @Test + public void testGetVoucherStatisticsList_periodInvalid() { + // 准备参数 + FmsVoucherStatisticsReqVO reqVO = buildStatisticsReqVO(); + reqVO.setStartMonth("2026-09").setEndMonth("2026-08"); + + // 调用,并断言异常 + assertServiceException(() -> voucherService.getVoucherStatisticsList(reqVO, 10L), + LEDGER_PERIOD_INVALID); + } + + @Test + public void testGetVoucherStatisticsList_rangeInvalid() { + // 准备参数 + FmsVoucherStatisticsReqVO reqVO = buildStatisticsReqVO(); + reqVO.setMinVoucherNumber(2).setMaxVoucherNumber(1); + + // 调用,并断言异常 + assertServiceException(() -> voucherService.getVoucherStatisticsList(reqVO, 10L), + VOUCHER_STATISTICS_RANGE_INVALID); + } + + // ========== 随机对象 ========== + + private FmsVoucherSaveReqVO buildSaveReqVO(String debitAmount, String creditAmount) { + FmsVoucherSaveReqVO reqVO = new FmsVoucherSaveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setVoucherWordId(11L); + reqVO.setVoucherNumber(1); + reqVO.setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)); + reqVO.setAttachmentCount(0); + reqVO.setEntries(Arrays.asList( + buildEntry("采购材料", bankSubject.getId(), debitAmount, null), + buildEntry("采购材料", payableSubject.getId(), null, creditAmount))); + return reqVO; + } + + private FmsVoucherStatisticsReqVO buildStatisticsReqVO() { + return new FmsVoucherStatisticsReqVO().setAccountSetId(1L) + .setStartMonth("2026-08").setEndMonth("2026-08").setMinLevel(1).setMaxLevel(1); + } + + private FmsVoucherImportExcelVO buildImportRow(Integer rowNumber, String subjectCode, + String digest, String debitAmount, String creditAmount) { + return new FmsVoucherImportExcelVO().setRowNumber(rowNumber) + .setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)).setVoucherWordName("记") + .setVoucherNumber(1).setAttachmentCount(2).setSubjectCode(subjectCode) + .setDigest(digest) + .setDebitAmount(debitAmount == null ? null : new BigDecimal(debitAmount)) + .setCreditAmount(creditAmount == null ? null : new BigDecimal(creditAmount)); + } + + private FmsVoucherTidyReqVO buildTidyReqVO( + Long voucherWordId, Integer startNumber, Integer type) { + FmsVoucherTidyReqVO reqVO = new FmsVoucherTidyReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + reqVO.setVoucherWordId(voucherWordId); + reqVO.setStartNumber(startNumber); + reqVO.setType(type); + return reqVO; + } + + private FmsVoucherMoveReqVO buildMoveReqVO(Integer sourceNumber, Integer targetNumber) { + FmsVoucherMoveReqVO reqVO = new FmsVoucherMoveReqVO(); + reqVO.setAccountSetId(1L); + reqVO.setMonth("2026-08"); + reqVO.setVoucherWordId(11L); + reqVO.setSourceNumber(sourceNumber); + reqVO.setTargetNumber(targetNumber); + return reqVO; + } + + private FmsVoucherEntrySaveReqVO buildEntry( + String digest, Long subjectId, String debitAmount, String creditAmount) { + FmsVoucherEntrySaveReqVO entry = new FmsVoucherEntrySaveReqVO(); + entry.setDigest(digest); + entry.setSubjectId(subjectId); + entry.setDebitAmount(debitAmount == null ? null : new BigDecimal(debitAmount)); + entry.setCreditAmount(creditAmount == null ? null : new BigDecimal(creditAmount)); + entry.setAuxiliaries(Collections.emptyList()); + return entry; + } + + private FmsSubjectDO buildSubject(Long id, String code, String name) { + return randomPojo(FmsSubjectDO.class, subject -> subject.setId(id).setAccountSetId(1L) + .setCode(code).setName(name).setParentId(FmsSubjectDO.PARENT_ID_ROOT).setLevel(1) + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setQuantityAccounting(false) + .setAuxiliaryTypeIds(Collections.emptyList()).setCurrencyIds(Collections.emptyList())); + } + + private FmsVoucherEntryDO buildVoucherEntry(Long voucherId, FmsSubjectDO subject, + BigDecimal debitAmount, BigDecimal creditAmount) { + return randomPojo(FmsVoucherEntryDO.class, entry -> entry.setId(null) + .setVoucherId(voucherId).setAccountSetId(1L).setSubjectId(subject.getId()) + .setSubjectCode(subject.getCode()).setSubjectName(subject.getName()).setDigest("测试摘要") + .setSort(1).setDebitAmount(debitAmount).setCreditAmount(creditAmount) + .setQuantity(BigDecimal.ZERO).setUnitPrice(BigDecimal.ZERO) + .setAssistCombinationId(null).setAuxiliaries(Collections.emptyList())); + } + + private FmsVoucherDO buildVoucher(Long accountSetId, Integer status) { + return new FmsVoucherDO().setAccountSetId(accountSetId).setVoucherWordId(11L) + .setVoucherNumber(1).setVoucherTime(LocalDateTime.of(2026, 8, 2, 0, 0)) + .setAttachmentUrls(Collections.emptyList()).setDebitAmount(new BigDecimal("100.00")) + .setCreditAmount(new BigDecimal("100.00")).setTotal(new BigDecimal("100.00")).setStatus(status); + } + +} diff --git a/yudao-module-fms/src/test/resources/application-unit-test.yaml b/yudao-module-fms/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..f0fcc54 --- /dev/null +++ b/yudao-module-fms/src/test/resources/application-unit-test.yaml @@ -0,0 +1,36 @@ +spring: + main: + lazy-initialization: true + banner-mode: off + allow-bean-definition-overriding: true + +--- #################### 数据库相关配置 #################### + +spring: + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value,day,year,month; + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true + initial-size: 1 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + data: + redis: + host: 127.0.0.1 + port: 16379 + database: 0 + +mybatis: + lazy-initialization: true + +--- #################### 芋道相关配置 #################### + +yudao: + info: + base-package: cn.iocoder.yudao.module diff --git a/yudao-module-fms/src/test/resources/logback.xml b/yudao-module-fms/src/test/resources/logback.xml new file mode 100644 index 0000000..8313843 --- /dev/null +++ b/yudao-module-fms/src/test/resources/logback.xml @@ -0,0 +1,2 @@ + + diff --git a/yudao-module-fms/src/test/resources/sql/clean.sql b/yudao-module-fms/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..26da83c --- /dev/null +++ b/yudao-module-fms/src/test/resources/sql/clean.sql @@ -0,0 +1,29 @@ +DELETE FROM "fms_closing_voucher"; +DELETE FROM "fms_closing_template"; +DELETE FROM "fms_closing"; +DELETE FROM "fms_closing_period"; +DELETE FROM "fms_cash_flow_extend_data"; +DELETE FROM "fms_cash_flow_extend_config"; +DELETE FROM "fms_cash_flow_statement_report"; +DELETE FROM "fms_cash_flow_statement_config"; +DELETE FROM "fms_income_statement_report"; +DELETE FROM "fms_income_statement_config"; +DELETE FROM "fms_balance_sheet_report"; +DELETE FROM "fms_balance_sheet_config"; +DELETE FROM "fms_report_template"; +DELETE FROM "fms_voucher_entry"; +DELETE FROM "fms_voucher"; +DELETE FROM "fms_voucher_template"; +DELETE FROM "fms_voucher_template_category"; +DELETE FROM "fms_digest"; +DELETE FROM "fms_finance_indicator"; +DELETE FROM "fms_voucher_word"; +DELETE FROM "fms_initial_balance"; +DELETE FROM "fms_assist_combination"; +DELETE FROM "fms_auxiliary_item"; +DELETE FROM "fms_auxiliary_type"; +DELETE FROM "fms_subject"; +DELETE FROM "fms_subject_template"; +DELETE FROM "fms_currency"; +DELETE FROM "fms_account_user"; +DELETE FROM "fms_account_set"; diff --git a/yudao-module-fms/src/test/resources/sql/create_tables.sql b/yudao-module-fms/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..20246de --- /dev/null +++ b/yudao-module-fms/src/test/resources/sql/create_tables.sql @@ -0,0 +1,619 @@ +-- FMS 单元测试表结构,与 sql/mysql/fms.sql 业务字段保持同步 + +CREATE TABLE IF NOT EXISTS "fms_account_set" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "company_code" varchar(64) DEFAULT NULL, + "company_name" varchar(255) DEFAULT NULL, + "company_profile" varchar(500) DEFAULT NULL, + "industry" varchar(255) DEFAULT NULL, + "location" varchar(255) DEFAULT NULL, + "legal_representative" varchar(255) DEFAULT NULL, + "legal_representative_id_number" varchar(255) DEFAULT NULL, + "business_license_number" varchar(255) DEFAULT NULL, + "organization_code" varchar(255) DEFAULT NULL, + "remark" varchar(500) DEFAULT NULL, + "contact_name" varchar(255) DEFAULT NULL, + "office_telephone" varchar(32) DEFAULT NULL, + "mobile" varchar(32) DEFAULT NULL, + "fax_number" varchar(32) DEFAULT NULL, + "qq_number" varchar(255) DEFAULT NULL, + "email" varchar(255) DEFAULT NULL, + "other_contact" varchar(255) DEFAULT NULL, + "address" varchar(255) DEFAULT NULL, + "currency_id" bigint DEFAULT NULL, + "start_time" timestamp DEFAULT NULL, + "standard" int DEFAULT NULL, + "initialized" bit NOT NULL DEFAULT FALSE, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + UNIQUE ("company_code", "deleted", "tenant_id") +); + +CREATE TABLE IF NOT EXISTS "fms_finance_parameter" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "account_set_id" bigint NOT NULL, + "level" int NOT NULL, + "subject_code_rule" varchar(64) NOT NULL, + "ledger_balance_mode" int NOT NULL, + "deficit_check" bit NOT NULL DEFAULT FALSE, + "voucher_review_required" bit NOT NULL DEFAULT FALSE, + "asset_period_locked" bit NOT NULL DEFAULT FALSE, + "taxpayer_name" varchar(255) DEFAULT NULL, + "taxpayer_number" varchar(64) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + UNIQUE ("account_set_id", "deleted", "tenant_id") +); + +CREATE TABLE IF NOT EXISTS "fms_account_user" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "account_set_id" bigint DEFAULT NULL, + "user_id" bigint DEFAULT NULL, + "default_status" bit NOT NULL DEFAULT FALSE, + "founder" bit NOT NULL DEFAULT FALSE, + "level" int NOT NULL DEFAULT 2, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_currency" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(64) DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "exchange_rate" decimal(18,6) NOT NULL DEFAULT 0, + "standard" bit NOT NULL DEFAULT FALSE, + "account_set_id" bigint DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + UNIQUE ("account_set_id", "code", "deleted", "tenant_id") +); + +CREATE TABLE IF NOT EXISTS "fms_subject" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(64) DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "parent_id" bigint NOT NULL DEFAULT 0, + "type" int DEFAULT NULL, + "category" int DEFAULT NULL, + "balance_direction" int DEFAULT NULL, + "quantity_unit" varchar(255) DEFAULT NULL, + "cash" bit NOT NULL DEFAULT FALSE, + "status" tinyint NOT NULL DEFAULT 0, + "level" int DEFAULT NULL, + "quantity_accounting" bit NOT NULL DEFAULT FALSE, + "account_set_id" bigint DEFAULT NULL, + "auxiliary_type_ids" varchar(10000) DEFAULT NULL, + "currency_ids" varchar(4000) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_subject_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(64) DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "parent_id" bigint NOT NULL DEFAULT 0, + "type" int DEFAULT NULL, + "category" int DEFAULT NULL, + "balance_direction" int DEFAULT NULL, + "quantity_unit" varchar(255) DEFAULT NULL, + "cash" bit NOT NULL DEFAULT FALSE, + "status" int DEFAULT NULL, + "level" int DEFAULT NULL, + "quantity_accounting" bit NOT NULL DEFAULT FALSE, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_auxiliary_type" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "system_preset" bit NOT NULL DEFAULT FALSE, + "account_set_id" bigint DEFAULT NULL, + "type" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + UNIQUE ("account_set_id", "name", "deleted", "tenant_id") +); + +CREATE TABLE IF NOT EXISTS "fms_auxiliary_item" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(64) DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "auxiliary_type_id" bigint DEFAULT NULL, + "status" int DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "remark" varchar(500) DEFAULT NULL, + "specification" varchar(255) DEFAULT NULL, + "unit" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + UNIQUE ("account_set_id", "auxiliary_type_id", "code", "deleted", "tenant_id") +); + +CREATE TABLE IF NOT EXISTS "fms_assist_combination" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "subject_id" bigint DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "items" varchar(10000) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_initial_balance" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "subject_id" bigint DEFAULT NULL, + "auxiliary_accounting" bit NOT NULL DEFAULT FALSE, + "opening_amount" decimal(18,2) NOT NULL DEFAULT 0, + "opening_quantity" decimal(18,4) NOT NULL DEFAULT 0, + "year_debit_amount" decimal(18,2) NOT NULL DEFAULT 0, + "year_debit_quantity" decimal(18,4) NOT NULL DEFAULT 0, + "year_credit_amount" decimal(18,2) NOT NULL DEFAULT 0, + "year_credit_quantity" decimal(18,4) NOT NULL DEFAULT 0, + "year_opening_amount" decimal(18,2) NOT NULL DEFAULT 0, + "year_opening_quantity" decimal(18,4) NOT NULL DEFAULT 0, + "profit_loss_amount" decimal(18,2) NOT NULL DEFAULT 0, + "profit_loss_quantity" decimal(18,4) NOT NULL DEFAULT 0, + "account_set_id" bigint DEFAULT NULL, + "assist_balances" varchar(10000) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + UNIQUE ("account_set_id", "subject_id", "deleted", "tenant_id") +); + +CREATE TABLE IF NOT EXISTS "fms_voucher" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "voucher_word_id" bigint DEFAULT NULL, + "voucher_number" int DEFAULT NULL, + "voucher_time" timestamp DEFAULT NULL, + "voucher_period" int GENERATED ALWAYS AS (YEAR("voucher_time") * 100 + MONTH("voucher_time")), + "attachment_urls" varchar(10000) DEFAULT NULL, + "attachment_count" int NOT NULL DEFAULT 0, + "debit_amount" decimal(18,2) NOT NULL DEFAULT 0, + "credit_amount" decimal(18,2) NOT NULL DEFAULT 0, + "total" decimal(18,2) NOT NULL DEFAULT 0.00, + "status" int DEFAULT NULL, + "reviewer_user_id" bigint DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_voucher_entry" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "digest" varchar(500) DEFAULT NULL, + "subject_name" varchar(255) DEFAULT NULL, + "quantity" decimal(18,4) NOT NULL DEFAULT 0, + "debit_amount" decimal(18,2) NOT NULL DEFAULT 0, + "credit_amount" decimal(18,2) NOT NULL DEFAULT 0, + "voucher_id" bigint DEFAULT NULL, + "subject_code" varchar(64) DEFAULT NULL, + "sort" int DEFAULT NULL, + "subject_id" bigint DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "unit_price" decimal(18,6) NOT NULL DEFAULT 0, + "assist_combination_id" bigint DEFAULT NULL, + "auxiliaries" varchar(10000) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_voucher_word" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "print_title" varchar(255) DEFAULT NULL, + "default_status" bit NOT NULL DEFAULT FALSE, + "sort" int NOT NULL DEFAULT 0, + "account_set_id" bigint DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_digest" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "content" varchar(500) NOT NULL, + "account_set_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_finance_indicator" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "account_set_id" bigint NOT NULL, + "name" varchar(100) NOT NULL, + "code" varchar(64) NOT NULL, + "type" int NOT NULL, + "formula" varchar(2000) NOT NULL, + "sort" int NOT NULL DEFAULT 0, + "status" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_voucher_template_category" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL, + "account_set_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_voucher_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL, + "category_id" bigint NOT NULL, + "entries" text NOT NULL, + "account_set_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_balance_sheet_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "remark" varchar(500) DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "sort" int DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "level" int DEFAULT NULL, + "row_id" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_balance_sheet_report" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "from_period" int DEFAULT NULL, + "to_period" int DEFAULT NULL, + "type" int DEFAULT NULL, + "level" int DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "remark" varchar(500) DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "sort" int DEFAULT NULL, + "opening_amount" decimal(18,2) NOT NULL DEFAULT 0, + "closing_amount" decimal(18,2) NOT NULL DEFAULT 0, + "account_set_id" bigint DEFAULT NULL, + "settled" bit NOT NULL DEFAULT FALSE, + "row_id" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_income_statement_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "sort" int DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "account_set_id" bigint DEFAULT NULL, + "level" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_income_statement_report" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" int DEFAULT NULL, + "from_period" int DEFAULT NULL, + "to_period" int DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "sort" int DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "current_amount" decimal(18,2) NOT NULL DEFAULT 0, + "year_amount" decimal(18,2) NOT NULL DEFAULT 0, + "account_set_id" bigint DEFAULT NULL, + "level" int DEFAULT NULL, + "settled" bit NOT NULL DEFAULT FALSE, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_cash_flow_statement_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "remark" varchar(500) DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "sort" int DEFAULT NULL, + "category" int DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "level" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_cash_flow_statement_report" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "from_period" int DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "remark" varchar(500) DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "current_amount" decimal(18,2) NOT NULL DEFAULT 0, + "year_amount" decimal(18,2) NOT NULL DEFAULT 0, + "sort" int DEFAULT NULL, + "category" int DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "to_period" int DEFAULT NULL, + "type" int DEFAULT NULL, + "level" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_cash_flow_extend_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "remark" varchar(500) DEFAULT NULL, + "category" int DEFAULT NULL, + "type" int DEFAULT NULL, + "current_amount" decimal(18,2) NOT NULL DEFAULT 0, + "year_amount" decimal(18,2) NOT NULL DEFAULT 0, + "editable" bit NOT NULL DEFAULT FALSE, + "account_set_id" bigint DEFAULT NULL, + "sort" int DEFAULT NULL, + "level" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_cash_flow_extend_data" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "remark" varchar(500) DEFAULT NULL, + "category" int DEFAULT NULL, + "current_amount" decimal(18,2) NOT NULL DEFAULT 0, + "year_amount" decimal(18,2) NOT NULL DEFAULT 0, + "from_period" int DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "account_set_id" bigint DEFAULT NULL, + "sort" int DEFAULT NULL, + "to_period" int DEFAULT NULL, + "type" int DEFAULT NULL, + "level" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_report_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "row_no" int DEFAULT NULL, + "formula" text, + "remark" varchar(500) DEFAULT NULL, + "editable" bit NOT NULL DEFAULT FALSE, + "sort" int DEFAULT NULL, + "row_id" int DEFAULT NULL, + "type" int DEFAULT NULL, + "category" int DEFAULT NULL, + "level" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_closing" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) DEFAULT NULL, + "period_end" bit NOT NULL DEFAULT FALSE, + "subject_id" bigint DEFAULT NULL, + "formula_rule" int DEFAULT NULL, + "time_type" int DEFAULT NULL, + "voucher_word_id" bigint DEFAULT NULL, + "digest" varchar(500) DEFAULT NULL, + "voucher_type" int DEFAULT NULL, + "prior_year_adjustment_subject_id" bigint DEFAULT NULL, + "adjustment_closing_subject_id" bigint DEFAULT NULL, + "other_closing_subject_id" bigint DEFAULT NULL, + "reverse_balance" bit NOT NULL DEFAULT FALSE, + "type" int DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "closing_day" int DEFAULT NULL, + "subject_rules" varchar(10000) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_closing_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "account_set_id" bigint DEFAULT NULL, + "preset_code" varchar(64) DEFAULT NULL, + "name" varchar(255) DEFAULT NULL, + "category" int DEFAULT NULL, + "period_end" bit NOT NULL DEFAULT FALSE, + "subject_id" bigint DEFAULT NULL, + "formula_rule" int DEFAULT NULL, + "time_type" int DEFAULT NULL, + "subject_rules" varchar(10000) DEFAULT NULL, + "sort" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +ALTER TABLE "fms_closing_template" + ADD "unique_active" int GENERATED ALWAYS AS (CASE WHEN "deleted" = FALSE THEN 1 ELSE NULL END); +CREATE UNIQUE INDEX IF NOT EXISTS "uk_fms_closing_template_active" + ON "fms_closing_template" ("account_set_id", "preset_code", "unique_active", "tenant_id"); + +CREATE TABLE IF NOT EXISTS "fms_closing_voucher" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "closing_id" bigint DEFAULT NULL, + "voucher_id" bigint DEFAULT NULL, + "voucher_time" timestamp DEFAULT NULL, + "amount" decimal(24, 6) NOT NULL DEFAULT 0, + "closed" bit NOT NULL DEFAULT FALSE, + "account_set_id" bigint DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "fms_closing_period" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "closing_time" timestamp DEFAULT NULL, + "account_set_id" bigint DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); diff --git a/yudao-module-hrm/pom.xml b/yudao-module-hrm/pom.xml new file mode 100644 index 0000000..750c2dc --- /dev/null +++ b/yudao-module-hrm/pom.xml @@ -0,0 +1,69 @@ + + + + cn.iocoder.boot + yudao + ${revision} + + 4.0.0 + yudao-module-hrm + jar + + ${project.artifactId} + + hrm 包下,人力资源管理(Human Resource Management) + 例如说:员工、招聘、考勤、薪资、社保、绩效等等 + + + + + cn.iocoder.boot + yudao-module-system + ${revision} + + + cn.iocoder.boot + yudao-module-bpm + ${revision} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-web + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + + + + + cn.iocoder.boot + yudao-spring-boot-starter-job + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceClockController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceClockController.java new file mode 100644 index 0000000..2721cee --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceClockController.java @@ -0,0 +1,191 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockShiftReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockShiftRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record.HrmAttendanceClockDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceGroupDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.service.attendance.record.HrmAttendanceClockService; +import cn.iocoder.yudao.module.hrm.service.attendance.config.HrmAttendanceGroupService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 打卡记录") +@RestController +@RequestMapping("/hrm/attendance/clock") +@Validated +public class HrmAttendanceClockController { + + @Resource + private HrmAttendanceClockService attendanceClockService; + @Resource + private HrmAttendanceGroupService attendanceGroupService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private DeptApi deptApi; + + @PostMapping("/create") + @Operation(summary = "创建打卡记录") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:create')") + public CommonResult createAttendanceClock(@Valid @RequestBody HrmAttendanceClockSaveReqVO createReqVO) { + return success(attendanceClockService.createAttendanceClock(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改打卡记录") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:update')") + public CommonResult updateAttendanceClock(@Valid @RequestBody HrmAttendanceClockSaveReqVO updateReqVO) { + attendanceClockService.updateAttendanceClock(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除打卡记录") + @Parameter(name = "id", description = "打卡记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:delete')") + public CommonResult deleteAttendanceClock(@RequestParam("id") Long id) { + attendanceClockService.deleteAttendanceClock(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除打卡记录") + @Parameter(name = "ids", description = "打卡记录编号列表", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:delete')") + public CommonResult deleteAttendanceClockList( + @RequestParam("ids") @NotEmpty(message = "打卡记录不能为空") List ids) { + attendanceClockService.deleteAttendanceClockList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得打卡记录") + @Parameter(name = "id", description = "打卡记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')") + public CommonResult getAttendanceClock(@RequestParam("id") Long id) { + HrmAttendanceClockDO attendanceClock = attendanceClockService.getAttendanceClock(id); + return success(attendanceClock == null ? null + : CollUtil.getFirst(buildAttendanceClockRespVOList(Collections.singletonList(attendanceClock)))); + } + + @GetMapping("/get-shift") + @Operation(summary = "获得员工实际班次和允许打卡时间") + @PreAuthorize("@ss.hasAnyPermissions('hrm:attendance:clock:create', 'hrm:attendance:clock:update')") + public CommonResult getAttendanceClockShift( + @Valid HrmAttendanceClockShiftReqVO reqVO) { + HrmAttendanceGroupDO.Shift shift = attendanceGroupService.getEmployeeAttendanceShift( + reqVO.getEmployeeId(), reqVO.getAttendanceTime()); + return success(shift == null ? null : buildAttendanceClockShiftRespVO(reqVO, shift)); + } + + @GetMapping("/page") + @Operation(summary = "获得打卡记录分页") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')") + public CommonResult> getAttendanceClockPage( + @Validated HrmAttendanceClockPageReqVO pageReqVO) { + PageResult pageResult = attendanceClockService.getAttendanceClockPage(pageReqVO); + return success(new PageResult<>(buildAttendanceClockRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出打卡记录") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportAttendanceClock(@Validated HrmAttendanceClockPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = buildAttendanceClockRespVOList( + attendanceClockService.getAttendanceClockList(exportReqVO)); + ExcelUtils.write(response, "打卡记录.xls", "数据", HrmAttendanceClockRespVO.class, list); + } + + // ==================== 拼接 VO ==================== + + private HrmAttendanceClockShiftRespVO buildAttendanceClockShiftRespVO( + HrmAttendanceClockShiftReqVO reqVO, HrmAttendanceGroupDO.Shift shift) { + if (shift.getStartTime() == null || shift.getEndTime() == null + || shift.getClockInStartTime() == null || shift.getClockInEndTime() == null + || shift.getClockOutStartTime() == null || shift.getClockOutEndTime() == null) { + return null; + } + LocalDate attendanceDate = reqVO.getAttendanceTime().toLocalDate(); + TimeRange shiftTimeRange = CollUtil.getFirst(LocalDateTimeUtils.buildDailyTimeRanges( + attendanceDate, attendanceDate, shift.getStartTime(), shift.getEndTime())); + TimeRange clockInTimeRange = LocalDateTimeUtils.findDailyTimeRange( + attendanceDate, shift.getClockInStartTime(), + shift.getClockInEndTime(), shiftTimeRange.getStartTime()); + TimeRange clockOutTimeRange = LocalDateTimeUtils.findDailyTimeRange( + attendanceDate, shift.getClockOutStartTime(), + shift.getClockOutEndTime(), shiftTimeRange.getEndTime()); + if (clockInTimeRange == null || clockOutTimeRange == null) { + return null; + } + return new HrmAttendanceClockShiftRespVO() + .setStartTime(shiftTimeRange.getStartTime()).setEndTime(shiftTimeRange.getEndTime()) + .setClockInStartTime(clockInTimeRange.getStartTime()).setClockInEndTime(clockInTimeRange.getEndTime()) + .setClockOutStartTime(clockOutTimeRange.getStartTime()).setClockOutEndTime(clockOutTimeRange.getEndTime()); + } + + private List buildAttendanceClockRespVOList(List clocks) { + if (CollUtil.isEmpty(clocks)) { + return Collections.emptyList(); + } + // 1. 获得员工和部门信息 + Map employeeMap = employeeService.getEmployeeMap( + convertSet(clocks, HrmAttendanceClockDO::getEmployeeId)); + Map deptMap = deptApi.getDeptMap(convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId)); + + // 2. 拼接响应 + return BeanUtils.toBean(clocks, HrmAttendanceClockRespVO.class, respVO -> { + MapUtils.findAndThen(employeeMap, respVO.getEmployeeId(), employee -> { + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber()) + .setDeptId(employee.getDeptId()).setPostName(employee.getPostName()); + }); + MapUtils.findAndThen(deptMap, respVO.getDeptId(), dept -> respVO.setDeptName(dept.getName())); + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceGroupController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceGroupController.java new file mode 100644 index 0000000..9a7a4af --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceGroupController.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group.HrmAttendanceGroupPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group.HrmAttendanceGroupRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group.HrmAttendanceGroupSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceGroupDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.service.attendance.config.HrmAttendanceGroupService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - HRM 考勤组") +@RestController +@RequestMapping("/hrm/attendance/group") +@Validated +public class HrmAttendanceGroupController { + + @Resource + private HrmAttendanceGroupService attendanceGroupService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private DeptApi deptApi; + + @PostMapping("/create") + @Operation(summary = "创建考勤组") + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:create')") + public CommonResult createAttendanceGroup(@Valid @RequestBody HrmAttendanceGroupSaveReqVO createReqVO) { + return success(attendanceGroupService.createAttendanceGroup(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改考勤组") + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:update')") + public CommonResult updateAttendanceGroup(@Valid @RequestBody HrmAttendanceGroupSaveReqVO updateReqVO) { + attendanceGroupService.updateAttendanceGroup(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除考勤组") + @Parameter(name = "id", description = "考勤组编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:delete')") + public CommonResult deleteAttendanceGroup(@RequestParam("id") Long id) { + attendanceGroupService.deleteAttendanceGroup(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得考勤组") + @Parameter(name = "id", description = "考勤组编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')") + public CommonResult getAttendanceGroup(@RequestParam("id") Long id) { + return success(buildAttendanceGroupRespVO(attendanceGroupService.getAttendanceGroup(id))); + } + + @GetMapping("/my") + @Operation(summary = "获得员工所在考勤组") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')") + public CommonResult getMyAttendanceGroup(@RequestParam("employeeId") Long employeeId) { + return success(buildAttendanceGroupRespVO(attendanceGroupService.getMyAttendanceGroup(employeeId))); + } + + @GetMapping("/page") + @Operation(summary = "获得考勤组分页") + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')") + public CommonResult> getAttendanceGroupPage( + @Validated HrmAttendanceGroupPageReqVO pageReqVO) { + PageResult pageResult = attendanceGroupService.getAttendanceGroupPage(pageReqVO); + return success(new PageResult<>( + buildAttendanceGroupRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得考勤组精简列表") + @PreAuthorize("@ss.hasPermission('hrm:attendance:group:query')") + public CommonResult> getAttendanceGroupSimpleList() { + return success(convertList(attendanceGroupService.getAttendanceGroupList(), + group -> new HrmAttendanceGroupRespVO().setId(group.getId()).setName(group.getName()))); + } + + // ==================== 拼接 VO ==================== + + private HrmAttendanceGroupRespVO buildAttendanceGroupRespVO(HrmAttendanceGroupDO group) { + if (group == null) { + return null; + } + return buildAttendanceGroupRespVOList(Collections.singletonList(group)).get(0); + } + + private List buildAttendanceGroupRespVOList(List list) { + Map deptMap = deptApi.getDeptMap( + convertSetByFlatMap(list, HrmAttendanceGroupDO::getDeptIds, Collection::stream)); + Map employeeMap = employeeService.getEmployeeMap( + convertSetByFlatMap(list, HrmAttendanceGroupDO::getEmployeeIds, Collection::stream)); + return convertList(list, group -> BeanUtils.toBean(group, HrmAttendanceGroupRespVO.class) + .setDeptNames(convertList(group.getDeptIds(), + deptId -> deptMap.containsKey(deptId) ? deptMap.get(deptId).getName() : null)) + .setEmployeeNames(convertList(group.getEmployeeIds(), + employeeId -> employeeMap.containsKey(employeeId) + ? employeeMap.get(employeeId).getName() : null))); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceHolidayController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceHolidayController.java new file mode 100644 index 0000000..40d3869 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceHolidayController.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday.HrmAttendanceHolidayPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday.HrmAttendanceHolidayRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday.HrmAttendanceHolidaySaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceHolidayDO; +import cn.iocoder.yudao.module.hrm.service.attendance.config.HrmAttendanceHolidayService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Tag(name = "管理后台 - HRM 考勤节假日") +@RestController +@RequestMapping("/hrm/attendance/holiday") +@Validated +public class HrmAttendanceHolidayController { + + @Resource + private HrmAttendanceHolidayService attendanceHolidayService; + + @PostMapping("/create") + @Operation(summary = "创建考勤节假日") + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:create')") + public CommonResult createAttendanceHoliday(@Valid @RequestBody HrmAttendanceHolidaySaveReqVO createReqVO) { + return success(attendanceHolidayService.createAttendanceHoliday(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改考勤节假日") + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:update')") + public CommonResult updateAttendanceHoliday(@Valid @RequestBody HrmAttendanceHolidaySaveReqVO updateReqVO) { + attendanceHolidayService.updateAttendanceHoliday(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除考勤节假日") + @Parameter(name = "id", description = "考勤节假日编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:delete')") + public CommonResult deleteAttendanceHoliday(@RequestParam("id") Long id) { + attendanceHolidayService.deleteAttendanceHoliday(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得考勤节假日") + @Parameter(name = "id", description = "考勤节假日编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:query')") + public CommonResult getAttendanceHoliday(@RequestParam("id") Long id) { + HrmAttendanceHolidayDO holiday = attendanceHolidayService.getAttendanceHoliday(id); + return success(BeanUtils.toBean(holiday, HrmAttendanceHolidayRespVO.class)); + } + + @GetMapping("/check") + @Operation(summary = "检查日期是否为考勤节假日") + @Parameter(name = "day", description = "日期", required = true) + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:query')") + public CommonResult checkAttendanceHoliday( + @RequestParam("day") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime day) { + HrmAttendanceHolidayDO holiday = attendanceHolidayService.checkAttendanceHoliday(day); + return success(BeanUtils.toBean(holiday, HrmAttendanceHolidayRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得考勤节假日分页") + @PreAuthorize("@ss.hasPermission('hrm:attendance:holiday:query')") + public CommonResult> getAttendanceHolidayPage( + @Validated HrmAttendanceHolidayPageReqVO pageReqVO) { + PageResult pageResult = attendanceHolidayService.getAttendanceHolidayPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, HrmAttendanceHolidayRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceLeaveController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceLeaveController.java new file mode 100644 index 0000000..a7c81b9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceLeaveController.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeavePageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeaveRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record.HrmAttendanceLeaveDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.service.attendance.record.HrmAttendanceLeaveService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 请假") +@RestController +@RequestMapping("/hrm/attendance/leave") +@Validated +public class HrmAttendanceLeaveController { + + @Resource + private HrmAttendanceLeaveService attendanceLeaveService; + @Resource + private HrmEmployeeService employeeService; + + @Resource + private DeptApi deptApi; + + @GetMapping("/get") + @Operation(summary = "获得请假") + @Parameter(name = "id", description = "请假记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasAnyPermissions('hrm:attendance:leave:query', " + + "'bpm:process-instance:query', 'bpm:task:query')") + public CommonResult getAttendanceLeave(@RequestParam("id") Long id) { + HrmAttendanceLeaveDO list = attendanceLeaveService.getAttendanceLeave(id); + return success(buildAttendanceLeaveRespVO(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得请假分页") + @PreAuthorize("@ss.hasPermission('hrm:attendance:leave:query')") + public CommonResult> getAttendanceLeavePage( + @Validated HrmAttendanceLeavePageReqVO pageReqVO) { + PageResult pageResult = attendanceLeaveService.getAttendanceLeavePage(pageReqVO); + return success(new PageResult<>(buildAttendanceLeaveRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出请假") + @PreAuthorize("@ss.hasPermission('hrm:attendance:leave:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportAttendanceLeave(@Validated HrmAttendanceLeavePageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = buildAttendanceLeaveRespVOList( + attendanceLeaveService.getAttendanceLeaveList(exportReqVO)); + ExcelUtils.write(response, "请假记录.xls", "数据", HrmAttendanceLeaveRespVO.class, list); + } + + // ==================== 拼接 VO ==================== + + private HrmAttendanceLeaveRespVO buildAttendanceLeaveRespVO(HrmAttendanceLeaveDO attendanceLeave) { + if (attendanceLeave == null) { + return null; + } + return buildAttendanceLeaveRespVOList(Collections.singletonList(attendanceLeave)).get(0); + } + + private List buildAttendanceLeaveRespVOList(List list) { + Map employeeMap = employeeService.getEmployeeMap( + convertSet(list, HrmAttendanceLeaveDO::getEmployeeId)); + Map deptMap = deptApi.getDeptMap(convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId)); + return convertList(list, attendanceLeave -> { + HrmAttendanceLeaveRespVO respVO = BeanUtils.toBean(attendanceLeave, HrmAttendanceLeaveRespVO.class); + HrmEmployeeDO employee = employeeMap.get(attendanceLeave.getEmployeeId()); + if (employee == null) { + return respVO; + } + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber()) + .setDeptId(employee.getDeptId()).setPostName(employee.getPostName()); + MapUtils.findAndThen(deptMap, employee.getDeptId(), dept -> respVO.setDeptName(dept.getName())); + return respVO; + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceStatisticsController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceStatisticsController.java new file mode 100644 index 0000000..d7bb6d1 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceStatisticsController.java @@ -0,0 +1,212 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceDailyDetailReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceDailyDetailRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceDailyOverviewRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthDailyOverviewRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthDetailRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthRecordPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthRecordRespVO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.service.attendance.statistics.HrmAttendanceStatisticsService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 考勤统计") +@RestController +@RequestMapping("/hrm/attendance/statistics") +@Validated +public class HrmAttendanceStatisticsController { + + @Resource + private HrmAttendanceStatisticsService attendanceStatisticsService; + + @Resource + private DeptApi deptApi; + + @GetMapping("/daily-detail") + @Operation(summary = "查询员工每日打卡明细") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')") + public CommonResult getAttendanceDailyDetail( + @Valid HrmAttendanceDailyDetailReqVO reqVO) { + return success(attendanceStatisticsService.getAttendanceDailyDetail(reqVO)); + } + + @GetMapping("/month-record-page") + @Operation(summary = "查询员工月度考勤汇总") + @PreAuthorize("@ss.hasPermission('hrm:attendance:statistics:query')") + public CommonResult> getAttendanceMonthRecordPage( + @Valid HrmAttendanceMonthRecordPageReqVO reqVO) { + PageResult pageResult = + attendanceStatisticsService.getAttendanceMonthRecordPage(reqVO); + fillAttendanceMonthRecordDeptName(pageResult.getList()); + return success(pageResult); + } + + @GetMapping("/month-daily-page") + @Operation(summary = "查询员工月度打卡概况") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:query')") + public CommonResult> getAttendanceMonthDailyOverviewPage( + @Valid HrmAttendanceMonthRecordPageReqVO reqVO) { + PageResult pageResult = + attendanceStatisticsService.getAttendanceMonthDailyOverviewPage(reqVO); + fillAttendanceMonthDailyOverview(pageResult.getList()); + return success(pageResult); + } + + @GetMapping("/month-record-export-excel") + @Operation(summary = "导出员工月度考勤汇总") + @PreAuthorize("@ss.hasPermission('hrm:attendance:statistics:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportAttendanceMonthRecord(@Validated HrmAttendanceMonthRecordPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = + attendanceStatisticsService.getAttendanceMonthRecordList(exportReqVO); + fillAttendanceMonthRecordDeptName(list); + ExcelUtils.write(response, "员工月度考勤汇总.xls", "数据", HrmAttendanceMonthRecordRespVO.class, list); + } + + @GetMapping("/month-daily-export-excel") + @Operation(summary = "导出员工月度打卡概况") + @PreAuthorize("@ss.hasPermission('hrm:attendance:clock:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportAttendanceMonthDailyOverview(@Validated HrmAttendanceMonthRecordPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = + attendanceStatisticsService.getAttendanceMonthDailyOverviewList(exportReqVO); + fillAttendanceMonthDailyOverview(list); + writeAttendanceMonthDailyOverview(response, + YearMonth.of(exportReqVO.getYear(), exportReqVO.getMonth()), list); + } + + @GetMapping("/month-detail") + @Operation(summary = "查询员工月度考勤详情") + @Parameters({ + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024"), + @Parameter(name = "year", description = "年份", required = true, example = "2026"), + @Parameter(name = "month", description = "月份", required = true, example = "8") + }) + @PreAuthorize("@ss.hasPermission('hrm:attendance:statistics:query')") + public CommonResult getAttendanceMonthDetail( + @RequestParam("employeeId") Long employeeId, + @RequestParam("year") Integer year, + @RequestParam("month") Integer month) { + HrmAttendanceMonthDetailRespVO detail = + attendanceStatisticsService.getAttendanceMonthDetail(employeeId, year, month); + fillAttendanceMonthRecordDeptName(Collections.singletonList(detail.getSummary())); + return success(detail); + } + + private void writeAttendanceMonthDailyOverview(HttpServletResponse response, YearMonth month, + List list) + throws IOException { + List> head = convertList( + Arrays.asList("员工编号", "员工姓名", "工号", "部门", "岗位"), Collections::singletonList); + for (int day = 1; day <= month.lengthOfMonth(); day++) { + head.add(Collections.singletonList(month.atDay(day).toString())); + } + ExcelUtils.write(response, "员工月度打卡概况.xls", "打卡概况", head, + buildAttendanceMonthDailyOverviewData(month, list)); + } + + private List> buildAttendanceMonthDailyOverviewData( + YearMonth month, List list) { + List> result = new ArrayList<>(list.size()); + for (HrmAttendanceMonthDailyOverviewRespVO row : list) { + List data = new ArrayList<>(Arrays.asList(row.getEmployeeId(), row.getEmployeeName(), + row.getJobNumber(), row.getDeptName(), row.getPostName())); + for (int day = 1; day <= month.lengthOfMonth(); day++) { + data.add(formatDailyClockOverview(row.getDailyClockMap().get(month.atDay(day)))); + } + result.add(data); + } + return result; + } + + private List buildDailyClockOverviewList( + HrmAttendanceDailyOverviewRespVO dailyOverview) { + if (CollUtil.isEmpty(dailyOverview.getClocks())) { + return Collections.singletonList(new HrmAttendanceDailyOverviewRespVO.OverviewItem() + .setText(StrUtil.emptyIfNull(dailyOverview.getAttendanceResult()))); + } + // 按打卡时间升序展示每日打卡概况 + dailyOverview.getClocks().sort(Comparator.comparing(HrmAttendanceClockRespVO::getClockTime)); + return convertList(dailyOverview.getClocks(), + clock -> new HrmAttendanceDailyOverviewRespVO.OverviewItem() + .setType(StrUtil.removeSuffix(StrUtil.emptyIfNull(DictFrameworkUtils.parseDictDataLabel( + DictTypeConstants.HRM_ATTENDANCE_CLOCK_TYPE, clock.getType())), "打卡")) + .setTime(clock.getClockTime() == null ? "-" : clock.getClockTime().toLocalTime().toString()) + .setStatus(StrUtil.emptyIfNull(DictFrameworkUtils.parseDictDataLabel( + DictTypeConstants.HRM_ATTENDANCE_CLOCK_STATUS, clock.getStatus())))); + } + + private String formatDailyClockOverview(HrmAttendanceDailyOverviewRespVO dailyOverview) { + if (dailyOverview == null) { + return ""; + } + return dailyOverview.getOverviews().stream() + .map(item -> StrUtil.isNotEmpty(item.getText()) ? item.getText() + : String.format("%s %s %s", item.getType(), item.getTime(), item.getStatus())) + .collect(Collectors.joining(";")); + } + + // ==================== 拼接 VO ==================== + + private void fillAttendanceMonthDailyOverview(List list) { + Map deptMap = deptApi.getDeptMap( + convertSet(list, HrmAttendanceMonthDailyOverviewRespVO::getDeptId)); + list.forEach(vo -> { + MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName())); + MapUtil.emptyIfNull(vo.getDailyClockMap()).values().forEach( + overview -> overview.setOverviews(buildDailyClockOverviewList(overview))); + }); + } + + private void fillAttendanceMonthRecordDeptName(List list) { + Map deptMap = deptApi.getDeptMap( + convertSet(list, HrmAttendanceMonthRecordRespVO::getDeptId)); + list.forEach(vo -> MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName()))); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockPageReqVO.java new file mode 100644 index 0000000..8d6df1d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockPageReqVO.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockSourceEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 打卡记录分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmAttendanceClockPageReqVO extends PageParam { + + @Schema(description = "员工姓名或工号,模糊匹配", example = "张三") + private String search; + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "部门编号数组", example = "100,101") + private List deptIds; + + @Schema(hidden = true) + private Collection employeeIds; + + @Schema(description = "考勤打卡类型", example = "1") + @InEnum(value = HrmAttendanceClockTypeEnum.class, message = "打卡类型必须是 {value}") + private Integer type; + + @Schema(description = "考勤打卡状态", example = "1") + @InEnum(value = HrmAttendanceClockStatusEnum.class, message = "打卡状态必须是 {value}") + private Integer status; + + @Schema(description = "来源类型数组", example = "[1, 2]") + @InEnum(value = HrmAttendanceClockSourceEnum.class, message = "打卡来源必须是 {value}") + private List sourceTypes; + + @Schema(description = "打卡地点,模糊匹配", example = "文三路") + private String address; + + @Schema(description = "上班日期范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] attendanceTime; + + @Schema(description = "实际打卡时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] clockTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockRespVO.java new file mode 100644 index 0000000..35c178d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockRespVO.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 打卡记录 Response VO") +@Data +@ExcelIgnoreUnannotated +public class HrmAttendanceClockRespVO { + + @Schema(description = "打卡记录编号", example = "1024") + @ExcelProperty(value = "记录编号", index = 0) + private Long id; + + @Schema(description = "打卡员工编号", example = "1001") + @ExcelProperty(value = "员工编号", index = 1) + private Long employeeId; + + @Schema(description = "员工姓名", example = "芋道") + @ExcelProperty(value = "员工姓名", index = 2) + private String employeeName; + + @Schema(description = "工号", example = "HRM001") + @ExcelProperty(value = "工号", index = 3) + private String jobNumber; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门名称", example = "研发部") + @ExcelProperty(value = "部门", index = 4) + private String deptName; + + @Schema(description = "职位名称", example = "Java 工程师") + @ExcelProperty(value = "岗位", index = 5) + private String postName; + + @Schema(description = "打卡时间") + @ExcelProperty(value = "打卡时间", index = 8) + private LocalDateTime clockTime; + + @Schema(description = "打卡类型", example = "1") + @ExcelProperty(value = "打卡类型", index = 6, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_CLOCK_TYPE) + private Integer type; + + @Schema(description = "应打卡时间") + @ExcelProperty(value = "应打卡时间", index = 7) + private LocalDateTime attendanceTime; + + @Schema(description = "打卡来源") + @ExcelProperty(value = "来源", index = 10, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_CLOCK_SOURCE) + private Integer sourceType; + + @Schema(description = "打卡状态") + @ExcelProperty(value = "打卡状态", index = 9, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_CLOCK_STATUS) + private Integer status; + + @Schema(description = "打卡阶段") + private Integer stage; + + @Schema(description = "打卡地址", example = "总部大楼") + @ExcelProperty(value = "地点", index = 11) + private String address; + + @Schema(description = "经度", example = "121.473701") + private BigDecimal longitude; + + @Schema(description = "纬度", example = "31.230416") + private BigDecimal latitude; + + @Schema(description = "WiFi 名称", example = "office_wifi") + private String ssid; + + @Schema(description = "WiFi MAC 地址", example = "00:11:22:33:44:55") + private String mac; + + @Schema(description = "备注", example = "手工补卡") + @ExcelProperty(value = "备注", index = 12) + private String remark; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockSaveReqVO.java new file mode 100644 index 0000000..41e50ce --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockSaveReqVO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 打卡记录保存 Request VO") +@Data +public class HrmAttendanceClockSaveReqVO { + + @Schema(description = "打卡记录编号", example = "1024") + private Long id; + + @Schema(description = "打卡员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @NotNull(message = "打卡员工不能为空") + private Long employeeId; + + @Schema(description = "打卡时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "打卡时间不能为空") + private LocalDateTime clockTime; + + @Schema(description = "打卡类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "打卡类型不能为空") + @InEnum(value = HrmAttendanceClockTypeEnum.class, message = "打卡类型必须是 {value}") + private Integer type; + + @Schema(description = "应打卡时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "应打卡时间不能为空") + private LocalDateTime attendanceTime; + + @Schema(description = "打卡地址", example = "总部大楼") + @Size(max = 255, message = "打卡地址不能超过 255 个字符") + private String address; + + @Schema(description = "经度", example = "121.473701") + @DecimalMin(value = "-180", message = "经度必须在 -180 到 180 之间") + @DecimalMax(value = "180", message = "经度必须在 -180 到 180 之间") + private BigDecimal longitude; + + @Schema(description = "纬度", example = "31.230416") + @DecimalMin(value = "-90", message = "纬度必须在 -90 到 90 之间") + @DecimalMax(value = "90", message = "纬度必须在 -90 到 90 之间") + private BigDecimal latitude; + + @Schema(description = "WiFi 名称", example = "office_wifi") + @Size(max = 50, message = "WiFi 名称不能超过 50 个字符") + private String ssid; + + @Schema(description = "WiFi MAC 地址", example = "00:11:22:33:44:55") + @Size(max = 50, message = "WiFi MAC 地址不能超过 50 个字符") + private String mac; + + @Schema(description = "备注", example = "手工补卡") + @Size(max = 255, message = "备注不能超过 255 个字符") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftReqVO.java new file mode 100644 index 0000000..0b03d7e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 员工实际班次 Request VO") +@Data +public class HrmAttendanceClockShiftReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工不能为空") + private Long employeeId; + + @Schema(description = "考勤时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "考勤时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime attendanceTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftRespVO.java new file mode 100644 index 0000000..c1ff2f9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockShiftRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工实际班次 Response VO") +@Data +public class HrmAttendanceClockShiftRespVO { + + @Schema(description = "上班时间") + private LocalDateTime startTime; + + @Schema(description = "下班时间") + private LocalDateTime endTime; + + @Schema(description = "上班打卡开始时间") + private LocalDateTime clockInStartTime; + + @Schema(description = "上班打卡结束时间") + private LocalDateTime clockInEndTime; + + @Schema(description = "下班打卡开始时间") + private LocalDateTime clockOutStartTime; + + @Schema(description = "下班打卡结束时间") + private LocalDateTime clockOutEndTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupPageReqVO.java new file mode 100644 index 0000000..db34028 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupPageReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - HRM 考勤组分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmAttendanceGroupPageReqVO extends PageParam { + + @Schema(description = "考勤组名称", example = "默认考勤组") + private String name; + + @Schema(description = "是否默认考勤组", example = "true") + private Boolean defaultStatus; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupRespVO.java new file mode 100644 index 0000000..f3e3609 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupRespVO.java @@ -0,0 +1,178 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 考勤组 Response VO") +@Data +public class HrmAttendanceGroupRespVO { + + @Schema(description = "考勤组编号", example = "1024") + private Long id; + + @Schema(description = "名称", example = "总部考勤组") + private String name; + + @Schema(description = "是否启用 WiFi 打卡") + private Boolean openWifiCard; + + @Schema(description = "是否启用定位打卡") + private Boolean openPointCard; + + @Schema(description = "是否法定节假日休息") + private Boolean rest; + + @Schema(description = "是否默认配置") + private Boolean defaultStatus; + + @Schema(description = "特殊日期设置数组") + private List specialDates; + + @Schema(description = "部门范围") + private List deptIds; + + @Schema(description = "适用部门名称") + private List deptNames; + + @Schema(description = "员工范围") + private List employeeIds; + + @Schema(description = "适用员工名称") + private List employeeNames; + + @Schema(description = "班次配置") + private List shifts; + + @Schema(description = "打卡地点") + private List points; + + @Schema(description = "打卡 WiFi") + private List wifis; + + @Schema(description = "扣款规则") + private DeductRule deductRule; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "管理后台 - HRM 考勤组特殊日期 Response VO") + @Data + public static class SpecialDate { + + @Schema(description = "日期类型", example = "1") + private Integer type; + + @Schema(description = "特殊日期") + private LocalDateTime date; + } + + @Schema(description = "管理后台 - HRM 考勤组班次配置 Response VO") + @Data + public static class Shift { + + @Schema(description = "工作日数组", example = "[1, 2, 3, 4, 5]") + private List weeks; + + @Schema(description = "上班时间", example = "09:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime startTime; + + @Schema(description = "下班时间", example = "18:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime endTime; + + @Schema(description = "上班打卡开始时间", example = "08:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime clockInStartTime; + + @Schema(description = "上班打卡结束时间", example = "10:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime clockInEndTime; + + @Schema(description = "下班打卡开始时间", example = "17:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime clockOutStartTime; + + @Schema(description = "下班打卡结束时间", example = "20:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime clockOutEndTime; + + @Schema(description = "休息开始时间", example = "12:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime restStartTime; + + @Schema(description = "休息结束时间", example = "13:00") + @JsonFormat(pattern = "HH:mm") + private LocalTime restEndTime; + + @Schema(description = "休息时间是否不计入工作时长") + private Boolean excludeRestTime; + } + + @Schema(description = "管理后台 - HRM 考勤组打卡地点 Response VO") + @Data + public static class Point { + + @Schema(description = "地点名称", example = "总部办公区") + private String name; + + @Schema(description = "定位地址", example = "北京市海淀区中关村大街") + private String address; + + @Schema(description = "纬度", example = "39.983424") + private BigDecimal latitude; + + @Schema(description = "经度", example = "116.322987") + private BigDecimal longitude; + + @Schema(description = "有效打卡半径,单位:米", example = "300") + private Integer radius; + } + + @Schema(description = "管理后台 - HRM 考勤组 WiFi 配置 Response VO") + @Data + public static class Wifi { + + @Schema(description = "WiFi 名称", example = "Office-WiFi") + private String ssid; + + @Schema(description = "MAC 地址", example = "00:11:22:33:44:55") + private String mac; + } + + @Schema(description = "管理后台 - HRM 考勤组扣款规则 Response VO") + @Data + public static class DeductRule { + + @Schema(description = "迟到扣款方式", example = "1") + private Integer lateMethod; + + @Schema(description = "迟到扣款金额", example = "10.00") + private BigDecimal lateDeductMoney; + + @Schema(description = "早退扣款方式", example = "1") + private Integer earlyMethod; + + @Schema(description = "早退扣款金额", example = "10.00") + private BigDecimal earlyDeductMoney; + + @Schema(description = "旷工扣款方式", example = "1") + private Integer absenteeismMethod; + + @Schema(description = "旷工扣款金额", example = "100.00") + private BigDecimal absenteeismDeductMoney; + + @Schema(description = "缺卡扣款方式", example = "1") + private Integer misscardMethod; + + @Schema(description = "缺卡扣款金额", example = "20.00") + private BigDecimal misscardDeductMoney; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupSaveReqVO.java new file mode 100644 index 0000000..aa476f8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/group/HrmAttendanceGroupSaveReqVO.java @@ -0,0 +1,396 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.group; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceAbsenteeismDeductMethodEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceLateEarlyDeductMethodEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceMisscardDeductMethodEnum; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Schema(description = "管理后台 - HRM 考勤组保存 Request VO") +@Data +public class HrmAttendanceGroupSaveReqVO { + + private static final Set POINT_RADIUS_VALUES = new HashSet<>( + Arrays.asList(100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000)); + + @Schema(description = "考勤组编号", example = "1024") + private Long id; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "总部考勤组") + @NotBlank(message = "考勤组名称不能为空") + @Size(max = 50, message = "考勤组名称不能超过 50 个字符") + @DiffLogField(name = "考勤组名称") + private String name; + + @Schema(description = "是否启用 WiFi 打卡", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否启用 WiFi 打卡不能为空") + @DiffLogField(name = "是否启用 WiFi 打卡") + private Boolean openWifiCard; + + @Schema(description = "是否启用定位打卡", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否启用定位打卡不能为空") + @DiffLogField(name = "是否启用定位打卡") + private Boolean openPointCard; + + @Schema(description = "是否法定节假日休息", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否法定节假日休息不能为空") + @DiffLogField(name = "是否法定节假日休息") + private Boolean rest; + + @Schema(description = "特殊日期设置数组", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "特殊日期设置不能为空") + @DiffLogField(name = "特殊日期设置") + private List specialDates; + + @Schema(description = "部门范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 2048]") + @NotNull(message = "部门范围不能为空") + @DiffLogField(name = "适用部门") + private List<@NotNull(message = "部门编号不能为空") Long> deptIds; + + @Schema(description = "员工范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 2048]") + @NotNull(message = "员工范围不能为空") + @DiffLogField(name = "适用员工") + private List<@NotNull(message = "员工编号不能为空") Long> employeeIds; + + @Schema(description = "班次配置", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "班次配置不能为空") + @DiffLogField(name = "班次配置") + private List shifts; + + @Schema(description = "打卡地点", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "打卡地点不能为空") + @DiffLogField(name = "打卡地点") + private List points; + + @Schema(description = "打卡 WiFi", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "打卡 WiFi 不能为空") + @DiffLogField(name = "打卡 WiFi") + private List wifis; + + @Schema(description = "扣款规则", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "扣款规则不能为空") + @DiffLogField(name = "扣款规则") + private DeductRule deductRule; + + @Schema(description = "管理后台 - HRM 考勤组特殊日期") + @Data + public static class SpecialDate { + + @Schema(description = "日期类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "特殊日期类型不能为空") + @InEnum(value = HrmAttendanceHolidayTypeEnum.class, message = "特殊日期类型必须是 {value}") + private Integer type; + + @Schema(description = "特殊日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "特殊日期不能为空") + private LocalDateTime date; + } + + @Schema(description = "管理后台 - HRM 考勤组班次配置") + @Data + public static class Shift { + + @Schema(description = "工作日数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3, 4, 5]") + @NotEmpty(message = "班次工作日不能为空") + private List<@NotNull(message = "班次工作日不能为空") + @Min(value = 1, message = "班次工作日必须在 1 到 7 之间") + @Max(value = 7, message = "班次工作日必须在 1 到 7 之间") Integer> weeks; + + @Schema(description = "上班时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "09:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "上班时间不能为空") + private LocalTime startTime; + + @Schema(description = "下班时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "18:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "下班时间不能为空") + private LocalTime endTime; + + @Schema(description = "上班打卡开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "08:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "上班打卡开始时间不能为空") + private LocalTime clockInStartTime; + + @Schema(description = "上班打卡结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "10:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "上班打卡结束时间不能为空") + private LocalTime clockInEndTime; + + @Schema(description = "下班打卡开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "17:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "下班打卡开始时间不能为空") + private LocalTime clockOutStartTime; + + @Schema(description = "下班打卡结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "20:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "下班打卡结束时间不能为空") + private LocalTime clockOutEndTime; + + @Schema(description = "休息开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "12:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "休息开始时间不能为空") + private LocalTime restStartTime; + + @Schema(description = "休息结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "13:00") + @JsonFormat(pattern = "HH:mm") + @NotNull(message = "休息结束时间不能为空") + private LocalTime restEndTime; + + @Schema(description = "休息时间是否不计入工作时长", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "休息时间计算方式不能为空") + private Boolean excludeRestTime; + } + + @Schema(description = "管理后台 - HRM 考勤组打卡地点") + @Data + public static class Point { + + @Schema(description = "地点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "总部办公区") + @NotBlank(message = "地点名称不能为空") + @Size(max = 50, message = "地点名称不能超过 50 个字符") + private String name; + + @Schema(description = "定位地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京市海淀区中关村大街") + @NotBlank(message = "定位地址不能为空") + @Size(max = 255, message = "定位地址不能超过 255 个字符") + private String address; + + @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "39.983424") + @NotNull(message = "纬度不能为空") + @DecimalMin(value = "-90", message = "纬度必须在 -90 到 90 之间") + @DecimalMax(value = "90", message = "纬度必须在 -90 到 90 之间") + private BigDecimal latitude; + + @Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED, example = "116.322987") + @NotNull(message = "经度不能为空") + @DecimalMin(value = "-180", message = "经度必须在 -180 到 180 之间") + @DecimalMax(value = "180", message = "经度必须在 -180 到 180 之间") + private BigDecimal longitude; + + @Schema(description = "有效打卡半径,单位:米", requiredMode = Schema.RequiredMode.REQUIRED, example = "300") + @NotNull(message = "有效打卡半径不能为空") + @Positive(message = "有效打卡半径必须大于 0") + private Integer radius; + } + + @Schema(description = "管理后台 - HRM 考勤组 WiFi 配置") + @Data + public static class Wifi { + + @Schema(description = "WiFi 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Office-WiFi") + @NotBlank(message = "WiFi 名称不能为空") + @Size(max = 50, message = "WiFi 名称不能超过 50 个字符") + private String ssid; + + @Schema(description = "MAC 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "00:11:22:33:44:55") + @NotBlank(message = "MAC 地址不能为空") + @Pattern(regexp = "^((([0-9a-fA-F]{2}:){5})|(([0-9a-fA-F]{2}-){5}))[0-9a-fA-F]{2}$", + message = "MAC 地址格式不正确") + private String mac; + } + + @Schema(description = "管理后台 - HRM 考勤组扣款规则") + @Data + public static class DeductRule { + + @Schema(description = "迟到扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "迟到扣款方式不能为空") + @InEnum(value = HrmAttendanceLateEarlyDeductMethodEnum.class, message = "迟到扣款方式必须是 {value}") + private Integer lateMethod; + + @Schema(description = "迟到扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.00") + @NotNull(message = "迟到扣款金额不能为空") + @DecimalMin(value = "0", message = "迟到扣款金额不能小于 0") + @Digits(integer = 8, fraction = 2, message = "迟到扣款金额最多 8 位整数和 2 位小数") + private BigDecimal lateDeductMoney; + + @Schema(description = "早退扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "早退扣款方式不能为空") + @InEnum(value = HrmAttendanceLateEarlyDeductMethodEnum.class, message = "早退扣款方式必须是 {value}") + private Integer earlyMethod; + + @Schema(description = "早退扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.00") + @NotNull(message = "早退扣款金额不能为空") + @DecimalMin(value = "0", message = "早退扣款金额不能小于 0") + @Digits(integer = 8, fraction = 2, message = "早退扣款金额最多 8 位整数和 2 位小数") + private BigDecimal earlyDeductMoney; + + @Schema(description = "旷工扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "旷工扣款方式不能为空") + @InEnum(value = HrmAttendanceAbsenteeismDeductMethodEnum.class, message = "旷工扣款方式必须是 {value}") + private Integer absenteeismMethod; + + @Schema(description = "旷工扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @NotNull(message = "旷工扣款金额不能为空") + @DecimalMin(value = "0", message = "旷工扣款金额不能小于 0") + @Digits(integer = 8, fraction = 2, message = "旷工扣款金额最多 8 位整数和 2 位小数") + private BigDecimal absenteeismDeductMoney; + + @Schema(description = "缺卡扣款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "缺卡扣款方式不能为空") + @InEnum(value = HrmAttendanceMisscardDeductMethodEnum.class, message = "缺卡扣款方式必须是 {value}") + private Integer misscardMethod; + + @Schema(description = "缺卡扣款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20.00") + @NotNull(message = "缺卡扣款金额不能为空") + @DecimalMin(value = "0", message = "缺卡扣款金额不能小于 0") + @Digits(integer = 8, fraction = 2, message = "缺卡扣款金额最多 8 位整数和 2 位小数") + private BigDecimal misscardDeductMoney; + } + + @AssertTrue(message = "不同班次不能配置重复工作日") + @JsonIgnore + public boolean isShiftWeeksUnique() { + if (CollUtil.isEmpty(shifts)) { + return true; + } + Set weeks = new HashSet<>(); + return shifts.stream().allMatch(shift -> shift != null && CollUtil.isNotEmpty(shift.getWeeks()) + && shift.getWeeks().stream().allMatch(weeks::add)); + } + + @AssertTrue(message = "班次上下班、打卡窗口或休息时间配置不正确") + @JsonIgnore + public boolean isShiftTimeValid() { + if (CollUtil.isEmpty(shifts)) { + return true; + } + return shifts.stream().allMatch(this::isShiftTimeValid); + } + + private boolean isShiftTimeValid(Shift shift) { + if (shift == null || shift.getStartTime() == null || shift.getEndTime() == null + || shift.getClockInStartTime() == null || shift.getClockInEndTime() == null + || shift.getClockOutStartTime() == null || shift.getClockOutEndTime() == null + || shift.getRestStartTime() == null || shift.getRestEndTime() == null + || shift.getExcludeRestTime() == null) { + // 交由内嵌字段的 @NotNull 输出具体校验错误 + return true; + } + if (shift.getStartTime().equals(shift.getEndTime()) + || shift.getClockInStartTime().equals(shift.getClockInEndTime()) + || shift.getClockOutStartTime().equals(shift.getClockOutEndTime())) { + return false; + } + LocalDate attendanceDate = LocalDate.of(2000, 1, 3); + TimeRange shiftTimeRange = CollUtil.getFirst(LocalDateTimeUtils.buildDailyTimeRanges( + attendanceDate, attendanceDate, shift.getStartTime(), shift.getEndTime())); + return LocalDateTimeUtils.findDailyTimeRange( + attendanceDate, shift.getClockInStartTime(), shift.getClockInEndTime(), + shiftTimeRange.getStartTime()) != null + && LocalDateTimeUtils.findDailyTimeRange( + attendanceDate, shift.getClockOutStartTime(), shift.getClockOutEndTime(), + shiftTimeRange.getEndTime()) != null + && isRestTimeValid(attendanceDate, shift, shiftTimeRange); + } + + private boolean isRestTimeValid(LocalDate attendanceDate, Shift shift, + TimeRange shiftTimeRange) { + if (Boolean.FALSE.equals(shift.getExcludeRestTime())) { + return true; + } + if (shift.getRestStartTime().equals(shift.getRestEndTime())) { + return false; + } + List restTimeRanges = LocalDateTimeUtils.buildDailyTimeRanges( + attendanceDate.minusDays(1), attendanceDate.plusDays(1), + shift.getRestStartTime(), shift.getRestEndTime()); + return CollUtil.findOne(restTimeRanges, restTimeRange -> + LocalDateTimeUtils.isAfterOrEqual(restTimeRange.getStartTime(), shiftTimeRange.getStartTime()) + && LocalDateTimeUtils.isBeforeOrEqual( + restTimeRange.getEndTime(), shiftTimeRange.getEndTime())) != null; + } + + @AssertTrue(message = "特殊日期不能重复") + @JsonIgnore + public boolean isSpecialDatesUnique() { + if (CollUtil.isEmpty(specialDates)) { + return true; + } + Set dates = new HashSet<>(); + return specialDates.stream().allMatch(specialDate -> specialDate != null && specialDate.getDate() != null + && dates.add(specialDate.getDate().toLocalDate())); + } + + @AssertTrue(message = "考勤组至少需要选择一个适用部门或员工") + @JsonIgnore + public boolean isScopeValid() { + if (deptIds == null || employeeIds == null) { + return true; + } + return CollUtil.isNotEmpty(deptIds) || CollUtil.isNotEmpty(employeeIds); + } + + @AssertTrue(message = "考勤组至少需要启用定位打卡或 WiFi 打卡") + @JsonIgnore + public boolean isClockMethodValid() { + if (openPointCard == null || openWifiCard == null) { + return true; + } + return openPointCard || openWifiCard; + } + + @AssertTrue(message = "启用定位打卡时,打卡地点不能为空") + @JsonIgnore + public boolean isPointSettingValid() { + if (Boolean.TRUE.equals(openPointCard)) { + return CollUtil.isNotEmpty(points); + } + return true; + } + + @AssertTrue(message = "启用 WiFi 打卡时,打卡 WiFi 不能为空") + @JsonIgnore + public boolean isWifiSettingValid() { + if (Boolean.TRUE.equals(openWifiCard)) { + return CollUtil.isNotEmpty(wifis); + } + return true; + } + + @AssertTrue(message = "有效打卡半径必须是 100 至 1000 米(间隔 100 米),或 2000、3000 米") + @JsonIgnore + public boolean isPointRadiusValid() { + if (CollUtil.isEmpty(points)) { + return true; + } + return points.stream().allMatch(point -> point != null + && point.getRadius() != null && POINT_RADIUS_VALUES.contains(point.getRadius())); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayPageReqVO.java new file mode 100644 index 0000000..84b817c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayPageReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 考勤节假日分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmAttendanceHolidayPageReqVO extends PageParam { + + @Schema(description = "日期范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] date; + + @Schema(description = "节假日类型", example = "1") + @InEnum(value = HrmAttendanceHolidayTypeEnum.class, message = "节假日类型必须是 {value}") + private Integer type; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayRespVO.java new file mode 100644 index 0000000..3e520e8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidayRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 考勤节假日 Response VO") +@Data +public class HrmAttendanceHolidayRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "日期") + private LocalDateTime date; + + @Schema(description = "考勤假期类型") + private Integer type; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidaySaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidaySaveReqVO.java new file mode 100644 index 0000000..d999235 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/holiday/HrmAttendanceHolidaySaveReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.holiday; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 考勤节假日保存 Request VO") +@Data +public class HrmAttendanceHolidaySaveReqVO { + + @Schema(description = "节假日编号", example = "1024") + private Long id; + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "日期不能为空") + private LocalDateTime date; + + @Schema(description = "节假日类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "类型不能为空") + @InEnum(value = HrmAttendanceHolidayTypeEnum.class, message = "节假日类型必须是 {value}") + private Integer type; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCancelReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCancelReqVO.java new file mode 100644 index 0000000..1089d4c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCancelReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工请假申请取消 Request VO") +@Data +public class HrmAttendanceLeaveCancelReqVO { + + @Schema(description = "请假记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "请假记录编号不能为空") + private Long id; + + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "行程取消") + @NotBlank(message = "取消原因不能为空") + @Size(max = 500, message = "取消原因不能超过 500 个字符") + private String reason; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCreateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCreateReqVO.java new file mode 100644 index 0000000..c01783c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveCreateReqVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工请假申请创建 Request VO") +@Data +public class HrmAttendanceLeaveCreateReqVO { + + @Schema(description = "请假类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "事假") + @NotBlank(message = "请假类型不能为空") + @Size(max = 64, message = "请假类型不能超过 64 个字符") + private String type; + + @Schema(description = "请假开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "请假开始时间不能为空") + private LocalDateTime startTime; + + @Schema(description = "请假结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "请假结束时间不能为空") + private LocalDateTime endTime; + + @Schema(description = "请假天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.00") + @NotNull(message = "请假天数不能为空") + @DecimalMin(value = "0.01", message = "请假天数必须大于 0") + @Digits(integer = 8, fraction = 2, message = "请假天数最多保留两位小数") + private BigDecimal day; + + @Schema(description = "请假事由", requiredMode = Schema.RequiredMode.REQUIRED, example = "个人事务") + @NotBlank(message = "请假事由不能为空") + @Size(max = 300, message = "请假事由不能超过 300 个字符") + private String reason; + + @Schema(description = "备注", example = "请审批") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @Schema(hidden = true) + @AssertTrue(message = "请假结束时间必须晚于请假开始时间") + @JsonIgnore + public boolean isTimeRangeValid() { + return startTime == null || endTime == null || endTime.isAfter(startTime); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeavePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeavePageReqVO.java new file mode 100644 index 0000000..dc39afc --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeavePageReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave; + +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 请假记录分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmAttendanceLeavePageReqVO extends SortablePageParam { + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "部门编号数组", example = "[100, 101]") + private List deptIds; + + @Schema(description = "员工姓名或工号", example = "张三") + private String employeeKeyword; + + @Schema(description = "请假类型数组", example = "[事假, 病假]") + private List types; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "开始时间") + private LocalDateTime[] startTime; + + @Schema(description = "审批状态", example = "2") + private Integer approvalStatus; + + @Schema(hidden = true) + private Collection employeeIds; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveRespVO.java new file mode 100644 index 0000000..d21dc36 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveRespVO.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 请假记录 Response VO") +@Data +@ExcelIgnoreUnannotated +public class HrmAttendanceLeaveRespVO { + + @Schema(description = "请假记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty(value = "记录编号", index = 0) + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @ExcelProperty(value = "员工编号", index = 1) + private Long employeeId; + + @Schema(description = "员工姓名", example = "张三") + @ExcelProperty(value = "员工姓名", index = 2) + private String employeeName; + + @Schema(description = "工号", example = "HRM001") + @ExcelProperty(value = "工号", index = 3) + private String jobNumber; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门名称", example = "研发部") + @ExcelProperty(value = "部门", index = 4) + private String deptName; + + @Schema(description = "职位名称", example = "Java 工程师") + @ExcelProperty(value = "岗位", index = 5) + private String postName; + + @Schema(description = "请假类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "事假") + @ExcelProperty(value = "请假类型", index = 6, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_ATTENDANCE_LEAVE_TYPE) + private String type; + + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "开始时间", index = 7) + private LocalDateTime startTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "结束时间", index = 8) + private LocalDateTime endTime; + + @Schema(description = "请假天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0") + @ExcelProperty(value = "请假天数", index = 9) + private BigDecimal day; + + @Schema(description = "请假事由", example = "个人事务") + @ExcelProperty(value = "请假事由", index = 10) + private String reason; + + @Schema(description = "备注") + @ExcelProperty(value = "备注", index = 11) + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "创建时间", index = 12) + private LocalDateTime createTime; + + @Schema(description = "审批状态", example = "1") + private Integer approvalStatus; + + @Schema(description = "流程实例编号") + private String processInstanceId; + + @Schema(description = "审批时间") + private LocalDateTime approvalTime; + + @Schema(description = "审批意见") + private String approvalReason; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailReqVO.java new file mode 100644 index 0000000..2b77cc8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 员工每日考勤明细 Request VO") +@Data +public class HrmAttendanceDailyDetailReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工不能为空") + private Long employeeId; + + @Schema(description = "考勤日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "考勤日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime attendanceTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailRespVO.java new file mode 100644 index 0000000..4708af0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailRespVO.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工每日考勤明细 Response VO") +@Data +@ExcelIgnoreUnannotated +public class HrmAttendanceDailyDetailRespVO { + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "员工姓名") + private String employeeName; + + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "职位名称") + private String postName; + + @ExcelProperty(value = "日期", index = 0) + @Schema(description = "日期") + private LocalDateTime attendanceTime; + + @ExcelProperty(value = "班次", index = 1) + @Schema(description = "班次") + private String shiftName; + + @Schema(description = "是否排班") + private Boolean scheduled; + + @Schema(description = "应打卡次数") + private Integer requiredClockCount; + + @Schema(description = "排班分钟数") + private Integer scheduledMinutes; + + @ExcelProperty(value = "缺卡次数", index = 8) + @Schema(description = "缺卡次数") + private Integer misscardCount; + + @Schema(description = "是否旷工") + private Boolean absenteeism; + + @Schema(description = "旷工分钟数") + private Integer absenteeismMinutes; + + @ExcelProperty(value = "旷工天数", index = 9) + @Schema(description = "旷工天数") + private BigDecimal absenteeismDays; + + @Schema(description = "是否请假") + private Boolean leaveStatus; + + @Schema(description = "请假分钟数") + private Integer leaveMinutes; + + @ExcelProperty(value = "请假天数", index = 10) + @Schema(description = "请假天数") + private BigDecimal leaveDays; + + @ExcelProperty(value = "考勤结果", index = 2) + @Schema(description = "考勤结果") + private String attendanceResult; + + @ExcelProperty(value = "打卡次数", index = 3) + @Schema(description = "打卡次数") + private Integer clockCount; + + @ExcelProperty(value = "迟到次数", index = 4) + @Schema(description = "迟到次数") + private Integer lateCount; + + @ExcelProperty(value = "迟到分钟", index = 5) + @Schema(description = "迟到分钟") + private Integer lateMinutes; + + @ExcelProperty(value = "早退次数", index = 6) + @Schema(description = "早退次数") + private Integer earlyCount; + + @ExcelProperty(value = "早退分钟", index = 7) + @Schema(description = "早退分钟") + private Integer earlyMinutes; + + @Schema(description = "打卡列表") + private List clockList; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyOverviewRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyOverviewRespVO.java new file mode 100644 index 0000000..ed72469 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyOverviewRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics; + +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工每日打卡概况 Response VO") +@Data +public class HrmAttendanceDailyOverviewRespVO { + + @Schema(description = "打卡记录") + private List clocks; + + @Schema(description = "考勤结果") + private String attendanceResult; + + @Schema(description = "打卡概况展示项") + private List overviews; + + @Schema(description = "管理后台 - HRM 员工每日打卡概况展示项 Response VO") + @Data + public static class OverviewItem { + + @Schema(description = "打卡类型") + private String type; + + @Schema(description = "打卡时间") + private String time; + + @Schema(description = "打卡状态") + private String status; + + @Schema(description = "考勤结果") + private String text; + + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDailyOverviewRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDailyOverviewRespVO.java new file mode 100644 index 0000000..f6cc003 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDailyOverviewRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.util.Map; + +@Schema(description = "管理后台 - HRM 员工月度打卡概况 Response VO") +@Data +public class HrmAttendanceMonthDailyOverviewRespVO { + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "员工姓名") + private String employeeName; + + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "职位名称") + private String postName; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "每日打卡概况,Key 为日期") + private Map dailyClockMap; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDetailRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDetailRespVO.java new file mode 100644 index 0000000..32e83d6 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthDetailRespVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics; + +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeaveRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工月度考勤详情 Response VO") +@Data +public class HrmAttendanceMonthDetailRespVO { + + @Schema(description = "月度汇总") + private HrmAttendanceMonthRecordRespVO summary; + + @Schema(description = "每日打卡明细") + private List dailyDetails; + + @Schema(description = "请假记录") + private List leaves; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordPageReqVO.java new file mode 100644 index 0000000..b65b106 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordPageReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工月度考勤汇总分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmAttendanceMonthRecordPageReqVO extends PageParam { + + @Schema(description = "员工姓名或工号,模糊匹配", example = "张三") + private String search; + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "部门编号数组", example = "100,101") + private List deptIds; + + @Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + @NotNull(message = "年份不能为空") + private Integer year; + + @Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + @NotNull(message = "月份不能为空") + @Min(value = 1, message = "月份必须在 1 到 12 之间") + @Max(value = 12, message = "月份必须在 1 到 12 之间") + private Integer month; + + @Schema(description = "是否全勤", example = "true") + private Boolean fullAttendance; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordRespVO.java new file mode 100644 index 0000000..5aa1eb7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordRespVO.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工月度考勤汇总 Response VO") +@Data +@ExcelIgnoreUnannotated +public class HrmAttendanceMonthRecordRespVO { + + @ExcelProperty(value = "员工编号", index = 0) + @Schema(description = "员工编号") + private Long employeeId; + + @ExcelProperty(value = "员工姓名", index = 1) + @Schema(description = "员工姓名") + private String employeeName; + + @ExcelProperty(value = "工号", index = 2) + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "部门编号") + private Long deptId; + + @ExcelProperty(value = "部门", index = 3) + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "职位名称") + @ExcelProperty(value = "岗位", index = 4) + private String postName; + + @Schema(description = "考勤组名称") + private String attendanceGroupName; + + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + @Schema(description = "员工状态") + private Integer employeeStatus; + + @Schema(description = "工作城市") + private String workCity; + + @ExcelProperty(value = "年份", index = 5) + @Schema(description = "年份") + private Integer year; + + @ExcelProperty(value = "月份", index = 6) + @Schema(description = "月份") + private Integer month; + + @ExcelProperty(value = "应出勤天数", index = 7) + @Schema(description = "应出勤天数") + private Integer attendDays; + + @ExcelProperty(value = "实际出勤天数", index = 8) + @Schema(description = "实际出勤天数") + private BigDecimal actualDays; + + @ExcelProperty(value = "迟到时长(分钟)", index = 9) + @Schema(description = "迟到时长(分钟)") + private Integer lateMinute; + + @ExcelProperty(value = "迟到次数", index = 10) + @Schema(description = "迟到次数") + private Integer lateCount; + + @ExcelProperty(value = "早退时长(分钟)", index = 11) + @Schema(description = "早退时长(分钟)") + private Integer earlyMinute; + + @ExcelProperty(value = "早退次数", index = 12) + @Schema(description = "早退次数") + private Integer earlyCount; + + @ExcelProperty(value = "缺卡次数", index = 13) + @Schema(description = "缺卡次数") + private Integer misscardCount; + + @ExcelProperty(value = "旷工天数", index = 14) + @Schema(description = "旷工天数") + private BigDecimal absenteeismDays; + + @ExcelProperty(value = "旷工时长(分钟)", index = 15) + @Schema(description = "旷工时长(分钟)") + private Integer absenteeismMinutes; + + @ExcelProperty(value = "请假天数", index = 16) + @Schema(description = "请假天数") + private BigDecimal leaveDays; + + @ExcelProperty(value = "请假时长(分钟)", index = 17) + @Schema(description = "请假时长(分钟)") + private Integer leaveMinutes; + + @ExcelProperty(value = "迟到扣款", index = 18) + @Schema(description = "迟到扣款") + private BigDecimal lateDeductAmount; + + @ExcelProperty(value = "早退扣款", index = 19) + @Schema(description = "早退扣款") + private BigDecimal earlyDeductAmount; + + @ExcelProperty(value = "缺卡扣款", index = 20) + @Schema(description = "缺卡扣款") + private BigDecimal misscardDeductAmount; + + @ExcelProperty(value = "旷工扣款", index = 21) + @Schema(description = "旷工扣款") + private BigDecimal absenteeismDeductAmount; + + @ExcelProperty(value = "考勤扣款合计", index = 22) + @Schema(description = "考勤扣款合计") + private BigDecimal attendanceDeductAmount; + + @Schema(description = "是否全勤") + private Boolean fullAttendance; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeCertificateController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeCertificateController.java new file mode 100644 index 0000000..770f50e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeCertificateController.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate.HrmEmployeeCertificateRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate.HrmEmployeeCertificateSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeCertificateDO; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeCertificateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工证书") +@RestController +@RequestMapping("/hrm/employee/certificate") +@Validated +public class HrmEmployeeCertificateController { + + @Resource + private HrmEmployeeCertificateService certificateService; + + @GetMapping("/list") + @Operation(summary = "获得员工证书列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getCertificateList( + @RequestParam("employeeId") Long employeeId) { + List certificates = certificateService.getCertificateListByEmployeeId(employeeId); + return success(BeanUtils.toBean(certificates, HrmEmployeeCertificateRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建员工证书") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult createCertificate( + @Valid @RequestBody HrmEmployeeCertificateSaveReqVO reqVO) { + return success(certificateService.createCertificate(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新员工证书") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult updateCertificate( + @Valid @RequestBody HrmEmployeeCertificateSaveReqVO reqVO) { + certificateService.updateCertificate(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工证书") + @Parameter(name = "id", description = "证书编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteCertificate(@RequestParam("id") Long id) { + certificateService.deleteCertificate(id); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeChangeRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeChangeRecordController.java new file mode 100644 index 0000000..a8cba82 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeChangeRecordController.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeChangeRecordRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeChangeRecordDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeChangeRecordService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - HRM 员工异动记录") +@RestController +@RequestMapping("/hrm/employee/change-record") +@Validated +public class HrmEmployeeChangeRecordController { + + @Resource + private HrmEmployeeChangeRecordService changeRecordService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private DeptApi deptApi; + + @GetMapping("/list") + @Operation(summary = "获得员工异动记录列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getEmployeeChangeRecordList( + @RequestParam("employeeId") Long employeeId) { + return success(buildEmployeeChangeRecordRespVOList( + changeRecordService.getEmployeeChangeRecordListByEmployeeId(employeeId))); + } + + // ==================== 拼接 VO ==================== + + private List buildEmployeeChangeRecordRespVOList( + List changeRecords) { + Map deptMap = deptApi.getDeptMap( + convertSetByFlatMap(changeRecords, changeRecord -> Stream.of(changeRecord.getOldDeptId(), changeRecord.getNewDeptId()))); + Map leaderEmployeeMap = employeeService.getEmployeeMap( + convertSetByFlatMap(changeRecords, changeRecord -> Stream.of(changeRecord.getOldLeaderEmployeeId(), changeRecord.getNewLeaderEmployeeId()))); + return BeanUtils.toBean(changeRecords, HrmEmployeeChangeRecordRespVO.class, changeRecord -> { + MapUtils.findAndThen(deptMap, changeRecord.getOldDeptId(), + dept -> changeRecord.setOldDeptName(dept.getName())); + MapUtils.findAndThen(deptMap, changeRecord.getNewDeptId(), + dept -> changeRecord.setNewDeptName(dept.getName())); + MapUtils.findAndThen(leaderEmployeeMap, changeRecord.getOldLeaderEmployeeId(), + leader -> changeRecord.setOldLeaderEmployeeName(leader.getName())); + MapUtils.findAndThen(leaderEmployeeMap, changeRecord.getNewLeaderEmployeeId(), + leader -> changeRecord.setNewLeaderEmployeeName(leader.getName())); + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContactController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContactController.java new file mode 100644 index 0000000..d68cfd7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContactController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact.HrmEmployeeContactRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact.HrmEmployeeContactSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeContactDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeContactService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工联系人") +@RestController +@RequestMapping("/hrm/employee/contact") +@Validated +public class HrmEmployeeContactController { + + @Resource + private HrmEmployeeContactService contactService; + + @GetMapping("/list") + @Operation(summary = "获得员工联系人列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getContactList( + @RequestParam("employeeId") Long employeeId) { + List contacts = contactService.getContactListByEmployeeId(employeeId); + return success(BeanUtils.toBean(contacts, HrmEmployeeContactRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建员工联系人") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult createContact(@Valid @RequestBody HrmEmployeeContactSaveReqVO reqVO) { + return success(contactService.createContact(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新员工联系人") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult updateContact(@Valid @RequestBody HrmEmployeeContactSaveReqVO reqVO) { + contactService.updateContact(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工联系人") + @Parameter(name = "id", description = "联系人编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteContact(@RequestParam("id") Long id) { + contactService.deleteContact(id); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java new file mode 100644 index 0000000..365b71b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract.HrmEmployeeContractRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract.HrmEmployeeContractSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeContractDO; +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeContractService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工合同") +@RestController +@RequestMapping("/hrm/employee/contract") +@Validated +public class HrmEmployeeContractController { + + @Resource + private HrmEmployeeContractService contractService; + + @GetMapping("/list") + @Operation(summary = "获得员工合同列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getContractList( + @RequestParam("employeeId") Long employeeId) { + List contracts = contractService.getContractListByEmployeeId(employeeId); + return success(BeanUtils.toBean(contracts, HrmEmployeeContractRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建员工合同") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult createContract(@Valid @RequestBody HrmEmployeeContractSaveReqVO reqVO) { + return success(contractService.createContract(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新员工合同") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult updateContract(@Valid @RequestBody HrmEmployeeContractSaveReqVO reqVO) { + contractService.updateContract(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工合同") + @Parameter(name = "id", description = "合同编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteContract(@RequestParam("id") Long id) { + contractService.deleteContract(id); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java new file mode 100644 index 0000000..be75c78 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java @@ -0,0 +1,477 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeConvertToFullTimeReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeDemoteReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeePromoteReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeRegularReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord.HrmEmployeeTransferReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeCancelQuitReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeConfirmEntryReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeCreateFromUserReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeDeptStatisticsRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeImportExcelVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeImportRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeePageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeNotifyRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeQuitReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeRehireReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeStatusCountRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeSalaryCardDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.employee.HrmInsuranceEmployeeInfoDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.config.HrmRecruitChannelDO; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusTabEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeSalaryCardService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.insurance.employee.HrmInsuranceEmployeeInfoService; +import cn.iocoder.yudao.module.hrm.service.recruit.config.HrmRecruitChannelService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getFirst; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sum; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.getYearsBetween; + +@Tag(name = "管理后台 - HRM 员工档案") +@RestController +@RequestMapping("/hrm/employee") +@Validated +public class HrmEmployeeController { + + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmRecruitChannelService recruitChannelService; + @Resource + private HrmInsuranceEmployeeInfoService insuranceEmployeeInfoService; + @Resource + private HrmEmployeeSalaryCardService employeeSalaryCardService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @PostMapping("/create") + @Operation(summary = "创建员工档案") + @PreAuthorize("@ss.hasPermission('hrm:employee:create')") + public CommonResult createEmployee(@Valid @RequestBody HrmEmployeeSaveReqVO createReqVO) { + return success(employeeService.createEmployee(createReqVO)); + } + + @PostMapping("/create-list") + @Operation(summary = "从未建档后台用户批量创建员工档案") + @PreAuthorize("@ss.hasPermission('hrm:employee:create')") + public CommonResult> createEmployeeList( + @RequestBody @NotEmpty(message = "待创建员工列表不能为空") + @Size(max = 100, message = "单次最多创建 100 名员工") + List<@Valid HrmEmployeeCreateFromUserReqVO> createReqVOList) { + return success(employeeService.createEmployeeList(createReqVOList)); + } + + @GetMapping("/bound-user-id-list") + @Operation(summary = "获得已经建立员工档案的后台用户编号列表") + @PreAuthorize("@ss.hasPermission('hrm:employee:create')") + public CommonResult> getBoundUserIdList() { + return success(employeeService.getBoundUserIdList()); + } + + @PostMapping("/send-profile-fill-message") + @Operation(summary = "发送填写员工档案通知") + @Parameter(name = "ids", description = "员工编号数组", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult sendEmployeeProfileFillMessage( + @RequestParam("ids") @NotEmpty(message = "员工不能为空") + @Size(max = 100, message = "单次最多通知 100 名员工") List ids) { + return success(employeeService.sendEmployeeProfileFillMessage(ids)); + } + + @PutMapping("/update") + @Operation(summary = "更新员工档案") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult updateEmployee(@Valid @RequestBody HrmEmployeeSaveReqVO updateReqVO) { + employeeService.updateEmployee(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工档案") + @Parameter(name = "id", description = "员工档案编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteEmployee(@RequestParam("id") Long id) { + employeeService.deleteEmployee(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除员工档案") + @Parameter(name = "ids", description = "员工档案编号数组", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteEmployeeList(@RequestParam("ids") @NotEmpty List ids) { + employeeService.deleteEmployeeList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得员工档案") + @Parameter(name = "id", description = "员工档案编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult getEmployee(@RequestParam("id") Long id) { + return success(buildEmployeeRespVO(employeeService.getEmployee(id))); + } + + @PutMapping("/confirm-entry") + @Operation(summary = "确认员工入职") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult confirmEmployeeEntry(@Valid @RequestBody HrmEmployeeConfirmEntryReqVO reqVO) { + employeeService.confirmEmployeeEntry(reqVO); + return success(true); + } + + @PostMapping("/rehire") + @Operation(summary = "办理离职员工再入职") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult rehireEmployee(@Valid @RequestBody HrmEmployeeRehireReqVO reqVO) { + employeeService.rehireEmployee(reqVO); + return success(true); + } + + @PostMapping("/regular") + @Operation(summary = "办理员工转正") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult regularEmployee(@Valid @RequestBody HrmEmployeeRegularReqVO reqVO) { + employeeService.regularEmployee(reqVO); + return success(true); + } + + @PostMapping("/transfer") + @Operation(summary = "办理员工调岗") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult transferEmployee(@Valid @RequestBody HrmEmployeeTransferReqVO reqVO) { + employeeService.transferEmployee(reqVO); + return success(true); + } + + @PostMapping("/promote") + @Operation(summary = "办理员工晋升") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult promoteEmployee(@Valid @RequestBody HrmEmployeePromoteReqVO reqVO) { + employeeService.promoteEmployee(reqVO); + return success(true); + } + + @PostMapping("/demote") + @Operation(summary = "办理员工降级") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult demoteEmployee(@Valid @RequestBody HrmEmployeeDemoteReqVO reqVO) { + employeeService.demoteEmployee(reqVO); + return success(true); + } + + @PostMapping("/convert-to-full-time") + @Operation(summary = "办理员工转为全职") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult convertEmployeeToFullTime( + @Valid @RequestBody HrmEmployeeConvertToFullTimeReqVO reqVO) { + employeeService.convertEmployeeToFullTime(reqVO); + return success(true); + } + + @PostMapping("/quit") + @Operation(summary = "办理员工离职") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult quitEmployee(@Valid @RequestBody HrmEmployeeQuitReqVO reqVO) { + employeeService.quitEmployee(reqVO); + return success(true); + } + + @PutMapping("/cancel-quit") + @Operation(summary = "取消员工离职") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult cancelEmployeeQuit(@Valid @RequestBody HrmEmployeeCancelQuitReqVO reqVO) { + employeeService.cancelEmployeeQuit(reqVO); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得员工档案分页") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getEmployeePage(@Validated HrmEmployeePageReqVO pageReqVO) { + PageResult pageResult = employeeService.getEmployeePage(pageReqVO); + return success(new PageResult<>(buildEmployeeRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/list") + @Operation(summary = "获得员工列表", description = "用于表单回显已选员工") + @Parameter(name = "ids", description = "员工编号列表", required = true, example = "[1024]") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getEmployeeList(@RequestParam("ids") @NotEmpty List ids) { + return success(buildEmployeeRespVOList(employeeService.getEmployeeListByIds(ids))); + } + + @GetMapping("/simple-page") + @Operation(summary = "获得员工精简分页", description = "只返回员工选择器需要的非敏感字段") + public CommonResult> getEmployeeSimplePage( + @Validated HrmEmployeePageReqVO pageReqVO) { + PageResult pageResult = employeeService.getEmployeePage(pageReqVO); + return success(new PageResult<>(buildEmployeeSimpleRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得员工精简列表", description = "用于员工选择器回显已选员工") + @Parameter(name = "ids", description = "员工编号列表", required = true, example = "[1024]") + public CommonResult> getEmployeeSimpleList( + @RequestParam("ids") @NotEmpty List ids) { + return success(buildEmployeeSimpleRespVOList(employeeService.getEmployeeListByIds(ids))); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出员工档案") + @PreAuthorize("@ss.hasPermission('hrm:employee:export')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void exportEmployee(@Validated HrmEmployeePageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = buildEmployeeExportRespVOList(employeeService.getEmployeeList( + BeanUtils.toBean(exportReqVO, HrmEmployeeListReqVO.class))); + ExcelUtils.write(response, "员工档案.xlsx", "数据", HrmEmployeeRespVO.class, list); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得员工档案导入模板") + @PreAuthorize("@ss.hasPermission('hrm:employee:import')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void importTemplate(HttpServletResponse response) throws IOException { + List list = Collections.singletonList( + HrmEmployeeImportExcelVO.builder() + .name("张三").jobNumber("HRM001").mobile("15601691300") + .country("中国").nation("汉族").idType(5).idNumber("OTHER-19900101") + .sex(1).email("hrm@example.com").nativePlace("浙江杭州") + .birthday(LocalDateTime.of(1990, 1, 1, 0, 0)) + .address("杭州市西湖区").highestEducation(8) + .deptId(1L).leaderJobNumber("HRM000") + .postName("Java 工程师").postLevel("P6") + .entryStatus(HrmEmployeeEntryStatusEnum.ACTIVE.getStatus()) + .status(HrmEmployeeStatusEnum.REGULAR.getStatus()) + .type(HrmEmployeeTypeEnum.FORMAL.getType()).probation(3) + .entryTime(LocalDateTime.of(2026, 7, 9, 9, 0)) + .regularTime(LocalDateTime.of(2026, 10, 9, 9, 0)) + .workCity("杭州").workAddress("西湖区").workDetailAddress("文三路") + .companyAgeStartTime(LocalDateTime.of(2026, 7, 9, 9, 0)) + .userMobile("15601691301") + .bankCardNumber("622202600001").bankAreaId(330100).bankName("招商银行") + .bankBranchName("杭州高新支行").firstSocialSecurity(false) + .firstAccumulationFund(false).socialSecurityNumber("SB20260001") + .accumulationFundNumber("GJJ20260001") + .socialSecurityStartMonth(LocalDateTime.of(2026, 7, 1, 0, 0)) + .schemeName("杭州标准参保方案") + .remark("示例员工").build() + ); + ExcelUtils.write(response, "员工档案导入模板.xlsx", "员工列表", HrmEmployeeImportExcelVO.class, list); + } + + @PostMapping("/import") + @Operation(summary = "导入员工档案") + @Parameters({ + @Parameter(name = "file", description = "Excel 文件", required = true), + @Parameter(name = "duplicateStrategy", description = "重复员工处理策略:1 跳过,2 覆盖,3 判失败", example = "3") + }) + @PreAuthorize("@ss.hasPermission('hrm:employee:import')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult importExcel( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "duplicateStrategy", required = false, defaultValue = "3") Integer duplicateStrategy) + throws Exception { + List list = ExcelUtils.read(file, HrmEmployeeImportExcelVO.class); + return success(employeeService.importEmployeeList(list, duplicateStrategy)); + } + + @GetMapping("/status-count") + @Operation(summary = "获得员工状态统计") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getEmployeeStatusCount( + @Validated HrmEmployeePageReqVO pageReqVO) { + Map countMap = employeeService.getEmployeeStatusCount(pageReqVO); + return success(convertList(Arrays.asList(HrmEmployeeStatusTabEnum.values()), + status -> new HrmEmployeeStatusCountRespVO(status.getStatus(), + countMap.getOrDefault(status.getStatus(), 0L)))); + } + + @GetMapping("/dept-statistics") + @Operation(summary = "获得员工部门统计") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getEmployeeDeptStatistics() { + Map> countMap = employeeService.getEmployeeCountMapByDeptAndType(); + return success(convertList(countMap.entrySet(), entry -> { + Map typeCountMap = entry.getValue(); + long activeCount = sum(typeCountMap.values(), Long::longValue); + return new HrmEmployeeDeptStatisticsRespVO(entry.getKey(), activeCount, + typeCountMap.getOrDefault(HrmEmployeeTypeEnum.FORMAL.getType(), 0L), + typeCountMap.getOrDefault(HrmEmployeeTypeEnum.INFORMAL.getType(), 0L)); + })); + } + + // ==================== 拼接 VO ==================== + + private HrmEmployeeRespVO buildEmployeeRespVO(HrmEmployeeDO employee) { + if (employee == null) { + return null; + } + return getFirst(buildEmployeeRespVOList(Collections.singletonList(employee))); + } + + private List buildEmployeeRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 1.1 后台用户信息 + Map userMap = adminUserApi.getUserMap( + convertSet(list, HrmEmployeeDO::getUserId)); + // 1.2 部门信息 + Map deptMap = deptApi.getDeptMap(convertSet(list, HrmEmployeeDO::getDeptId)); + // 1.3 直属上级员工信息 + Map leaderEmployeeMap = employeeService.getEmployeeMap( + convertSet(list, HrmEmployeeDO::getLeaderEmployeeId)); + // 1.4 招聘渠道信息 + Map channelMap = recruitChannelService.getRecruitChannelMap( + convertSet(list, HrmEmployeeDO::getChannelId)); + // 1.5 工资卡和社保资料 + Set employeeIds = convertSet(list, HrmEmployeeDO::getId); + Map salaryCardMap = + employeeSalaryCardService.getSalaryCardMap(employeeIds); + Map insuranceEmployeeInfoMap = + insuranceEmployeeInfoService.getInsuranceEmployeeInfoMap(employeeIds); + + // 2. 拼接响应 + LocalDate today = LocalDate.now(); + return BeanUtils.toBean(list, HrmEmployeeRespVO.class, vo -> { + MapUtils.findAndThen(userMap, vo.getUserId(), user -> vo.setUserNickname(user.getNickname())); + MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName())); + MapUtils.findAndThen(leaderEmployeeMap, vo.getLeaderEmployeeId(), + leader -> vo.setLeaderEmployeeName(leader.getName())); + MapUtils.findAndThen(channelMap, vo.getChannelId(), + channel -> vo.setChannelName(channel.getName())); + MapUtils.findAndThen(salaryCardMap, vo.getId(), salaryCard -> vo + .setSalaryCardNumber(salaryCard.getBankCardNumber()) + .setSalaryCardAreaId(salaryCard.getBankAreaId()) + .setSalaryCardAreaName(AreaUtils.format(salaryCard.getBankAreaId())) + .setSalaryCardBankName(salaryCard.getBankName()) + .setSalaryCardBankBranchName(salaryCard.getBankBranchName())); + MapUtils.findAndThen(insuranceEmployeeInfoMap, vo.getId(), socialSecurity -> vo + .setSocialSecurityNumber(socialSecurity.getSocialSecurityNumber()) + .setAccumulationFundNumber(socialSecurity.getAccumulationFundNumber())); + if (vo.getBirthday() != null) { + vo.setAge(getYearsBetween(vo.getBirthday().toLocalDate(), today)); + } + if (vo.getCompanyAgeStartTime() != null) { + vo.setCompanyAge(getYearsBetween(vo.getCompanyAgeStartTime().toLocalDate(), today)); + } + }); + } + + private List buildEmployeeSimpleRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 1. 查询部门和直属上级信息 + Map deptMap = deptApi.getDeptMap(convertSet(list, HrmEmployeeDO::getDeptId)); + Map leaderEmployeeMap = employeeService.getEmployeeMap( + convertSet(list, HrmEmployeeDO::getLeaderEmployeeId)); + + // 2. 拼接精简响应 + return convertList(list, employee -> { + HrmEmployeeRespVO vo = new HrmEmployeeRespVO() + .setId(employee.getId()).setName(employee.getName()) + .setJobNumber(employee.getJobNumber()).setMobile(employee.getMobile()) + .setDeptId(employee.getDeptId()).setLeaderEmployeeId(employee.getLeaderEmployeeId()) + .setPostName(employee.getPostName()).setPostLevel(employee.getPostLevel()) + .setEntryStatus(employee.getEntryStatus()).setStatus(employee.getStatus()); + MapUtils.findAndThen(deptMap, employee.getDeptId(), dept -> vo.setDeptName(dept.getName())); + MapUtils.findAndThen(leaderEmployeeMap, employee.getLeaderEmployeeId(), + leader -> vo.setLeaderEmployeeName(leader.getName())); + return vo; + }); + } + + private List buildEmployeeExportRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 1. 查询导出展示信息,不加载工资卡和社保等敏感聚合 + Map userMap = adminUserApi.getUserMap(convertSet(list, HrmEmployeeDO::getUserId)); + Map deptMap = deptApi.getDeptMap(convertSet(list, HrmEmployeeDO::getDeptId)); + Map leaderEmployeeMap = employeeService.getEmployeeMap( + convertSet(list, HrmEmployeeDO::getLeaderEmployeeId)); + Map channelMap = recruitChannelService.getRecruitChannelMap( + convertSet(list, HrmEmployeeDO::getChannelId)); + + // 2. 拼接安全导出字段 + return convertList(list, employee -> { + HrmEmployeeRespVO vo = new HrmEmployeeRespVO() + .setId(employee.getId()).setName(employee.getName()) + .setJobNumber(employee.getJobNumber()).setMobile(employee.getMobile()) + .setPostName(employee.getPostName()).setPostLevel(employee.getPostLevel()) + .setEntryStatus(employee.getEntryStatus()).setStatus(employee.getStatus()).setType(employee.getType()) + .setEntryTime(employee.getEntryTime()).setRegularTime(employee.getRegularTime()) + .setLeaveTime(employee.getLeaveTime()) + .setWorkCity(employee.getWorkCity()).setWorkAddress(employee.getWorkAddress()); + MapUtils.findAndThen(userMap, employee.getUserId(), user -> vo.setUserNickname(user.getNickname())); + MapUtils.findAndThen(deptMap, employee.getDeptId(), dept -> vo.setDeptName(dept.getName())); + MapUtils.findAndThen(leaderEmployeeMap, employee.getLeaderEmployeeId(), + leader -> vo.setLeaderEmployeeName(leader.getName())); + MapUtils.findAndThen(channelMap, employee.getChannelId(), channel -> vo.setChannelName(channel.getName())); + return vo; + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeEducationExperienceController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeEducationExperienceController.java new file mode 100644 index 0000000..abb6800 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeEducationExperienceController.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience.HrmEmployeeEducationExperienceRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience.HrmEmployeeEducationExperienceSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeEducationExperienceDO; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeEducationExperienceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工教育经历") +@RestController +@RequestMapping("/hrm/employee/education-experience") +@Validated +public class HrmEmployeeEducationExperienceController { + + @Resource + private HrmEmployeeEducationExperienceService educationExperienceService; + + @GetMapping("/list") + @Operation(summary = "获得员工教育经历列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getEducationExperienceList( + @RequestParam("employeeId") Long employeeId) { + List educationExperiences = + educationExperienceService.getEducationExperienceListByEmployeeId(employeeId); + return success(BeanUtils.toBean(educationExperiences, HrmEmployeeEducationExperienceRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建员工教育经历") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult createEducationExperience( + @Valid @RequestBody HrmEmployeeEducationExperienceSaveReqVO reqVO) { + return success(educationExperienceService.createEducationExperience(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新员工教育经历") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult updateEducationExperience( + @Valid @RequestBody HrmEmployeeEducationExperienceSaveReqVO reqVO) { + educationExperienceService.updateEducationExperience(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工教育经历") + @Parameter(name = "id", description = "教育经历编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteEducationExperience(@RequestParam("id") Long id) { + educationExperienceService.deleteEducationExperience(id); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFieldConfigController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFieldConfigController.java new file mode 100644 index 0000000..48c3f2a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFieldConfigController.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config.HrmEmployeeArchiveFieldConfigSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config.HrmEmployeeCreateFieldConfigSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config.HrmEmployeeFieldConfigRespVO; +import cn.iocoder.yudao.module.hrm.service.employee.config.HrmEmployeeFieldConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工字段配置") +@RestController +@RequestMapping("/hrm/employee/config") +@Validated +public class HrmEmployeeFieldConfigController { + + @Resource + private HrmEmployeeFieldConfigService employeeFieldConfigService; + + @GetMapping("/create-field/list") + @Operation(summary = "获得新建员工字段配置") + @Parameter(name = "entryStatus", description = "入职状态:1 在职,2 待入职", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('hrm:employee:config:query') or @ss.hasPermission('hrm:employee:create')") + public CommonResult> getEmployeeCreateFieldConfigList( + @RequestParam("entryStatus") Integer entryStatus) { + return success(employeeFieldConfigService.getEmployeeCreateFieldConfigList(entryStatus)); + } + + @PutMapping("/create-field/save") + @Operation(summary = "保存新建员工字段配置") + @PreAuthorize("@ss.hasPermission('hrm:employee:config:update')") + public CommonResult saveEmployeeCreateFieldConfig( + @Valid @RequestBody HrmEmployeeCreateFieldConfigSaveReqVO reqVO) { + employeeFieldConfigService.saveEmployeeCreateFieldConfig(reqVO); + return success(true); + } + + @GetMapping("/archive-field/list") + @Operation(summary = "获得员工档案字段配置") + @PreAuthorize("@ss.hasPermission('hrm:employee:config:query')") + public CommonResult> getEmployeeArchiveFieldConfigList() { + return success(employeeFieldConfigService.getEmployeeArchiveFieldConfigList()); + } + + @PutMapping("/archive-field/save") + @Operation(summary = "保存员工档案字段配置") + @PreAuthorize("@ss.hasPermission('hrm:employee:config:update')") + public CommonResult saveEmployeeArchiveFieldConfig( + @Valid @RequestBody HrmEmployeeArchiveFieldConfigSaveReqVO reqVO) { + employeeFieldConfigService.saveEmployeeArchiveFieldConfig(reqVO); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFileController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFileController.java new file mode 100644 index 0000000..3e54592 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeFileController.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file.HrmEmployeeFileRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file.HrmEmployeeFileSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeFileDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeFileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工材料附件") +@RestController +@RequestMapping("/hrm/employee/file") +@Validated +public class HrmEmployeeFileController { + + @Resource + private HrmEmployeeFileService employeeFileService; + + @GetMapping("/list") + @Operation(summary = "获得员工材料附件列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getEmployeeFileList( + @RequestParam("employeeId") Long employeeId) { + List employeeFiles = employeeFileService.getEmployeeFileList(employeeId); + return success(BeanUtils.toBean(employeeFiles, HrmEmployeeFileRespVO.class)); + } + + @PutMapping("/save") + @Operation(summary = "保存员工材料附件") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult saveEmployeeFiles(@Valid @RequestBody HrmEmployeeFileSaveReqVO reqVO) { + employeeFileService.saveEmployeeFiles(reqVO); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeePersonalNoteController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeePersonalNoteController.java new file mode 100644 index 0000000..8dfacb5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeePersonalNoteController.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.personalnote.HrmEmployeePersonalNoteCreateReqVO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeePersonalNoteService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工个人备忘") +@RestController +@RequestMapping("/hrm/employee/personal-note") +@Validated +public class HrmEmployeePersonalNoteController { + + @Resource + private HrmEmployeePersonalNoteService personalNoteService; + @Resource + private HrmEmployeeService employeeService; + + @PostMapping("/create") + @Operation(summary = "创建员工个人备忘") + @PreAuthorize("@ss.hasPermission('hrm:employee:personal-note:create')") + public CommonResult createPersonalNote( + @Valid @RequestBody HrmEmployeePersonalNoteCreateReqVO reqVO) { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + return success(personalNoteService.createPersonalNote(employeeId, reqVO)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工个人备忘") + @Parameter(name = "id", description = "员工个人备忘编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:personal-note:delete')") + public CommonResult deletePersonalNote(@RequestParam("id") Long id) { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + personalNoteService.deletePersonalNote(employeeId, id); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeQuitInfoController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeQuitInfoController.java new file mode 100644 index 0000000..b26172e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeQuitInfoController.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.quitinfo.HrmEmployeeQuitInfoRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeQuitInfoDO; +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeQuitInfoService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工离职信息") +@RestController +@RequestMapping("/hrm/employee/quit-info") +@Validated +public class HrmEmployeeQuitInfoController { + + @Resource + private HrmEmployeeQuitInfoService quitInfoService; + + @GetMapping("/get") + @Operation(summary = "获得员工离职信息") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult getQuitInfo( + @RequestParam("employeeId") Long employeeId) { + HrmEmployeeQuitInfoDO quitInfo = quitInfoService.getQuitInfoByEmployeeId(employeeId); + return success(BeanUtils.toBean(quitInfo, HrmEmployeeQuitInfoRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeSalaryCardController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeSalaryCardController.java new file mode 100644 index 0000000..854d10c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeSalaryCardController.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.salarycard.HrmEmployeeSalaryCardRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.salarycard.HrmEmployeeSalaryCardSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeSalaryCardDO; +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeSalaryCardService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工工资卡") +@RestController +@RequestMapping("/hrm/employee/salary-card") +@Validated +public class HrmEmployeeSalaryCardController { + + @Resource + private HrmEmployeeSalaryCardService salaryCardService; + + @GetMapping("/get") + @Operation(summary = "获得员工工资卡") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult getEmployeeSalaryCard( + @RequestParam("employeeId") Long employeeId) { + HrmEmployeeSalaryCardDO salaryCard = salaryCardService.getSalaryCardByEmployeeId(employeeId); + if (salaryCard == null) { + return success(null); + } + HrmEmployeeSalaryCardRespVO respVO = BeanUtils.toBean(salaryCard, HrmEmployeeSalaryCardRespVO.class) + .setBankAreaName(AreaUtils.format(salaryCard.getBankAreaId())); + return success(respVO); + } + + @PutMapping("/save") + @Operation(summary = "保存员工工资卡") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult saveEmployeeSalaryCard( + @Valid @RequestBody HrmEmployeeSalaryCardSaveReqVO reqVO) { + return success(salaryCardService.saveSalaryCard(reqVO)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工工资卡") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult deleteEmployeeSalaryCard(@RequestParam("employeeId") Long employeeId) { + salaryCardService.deleteSalaryCardByEmployeeId(employeeId); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeTrainingExperienceController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeTrainingExperienceController.java new file mode 100644 index 0000000..b90bf12 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeTrainingExperienceController.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.trainingexperience.HrmEmployeeTrainingExperienceRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.trainingexperience.HrmEmployeeTrainingExperienceSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeTrainingExperienceDO; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeTrainingExperienceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工培训经历") +@RestController +@RequestMapping("/hrm/employee/training-experience") +@Validated +public class HrmEmployeeTrainingExperienceController { + + @Resource + private HrmEmployeeTrainingExperienceService trainingExperienceService; + + @GetMapping("/list") + @Operation(summary = "获得员工培训经历列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getTrainingExperienceList( + @RequestParam("employeeId") Long employeeId) { + List trainingExperiences = + trainingExperienceService.getTrainingExperienceListByEmployeeId(employeeId); + return success(BeanUtils.toBean(trainingExperiences, HrmEmployeeTrainingExperienceRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建员工培训经历") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult createTrainingExperience( + @Valid @RequestBody HrmEmployeeTrainingExperienceSaveReqVO reqVO) { + return success(trainingExperienceService.createTrainingExperience(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新员工培训经历") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult updateTrainingExperience( + @Valid @RequestBody HrmEmployeeTrainingExperienceSaveReqVO reqVO) { + trainingExperienceService.updateTrainingExperience(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工培训经历") + @Parameter(name = "id", description = "培训经历编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteTrainingExperience(@RequestParam("id") Long id) { + trainingExperienceService.deleteTrainingExperience(id); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeWorkExperienceController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeWorkExperienceController.java new file mode 100644 index 0000000..47d23b7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeWorkExperienceController.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.workexperience.HrmEmployeeWorkExperienceRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.workexperience.HrmEmployeeWorkExperienceSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeWorkExperienceDO; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeWorkExperienceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工工作经历") +@RestController +@RequestMapping("/hrm/employee/work-experience") +@Validated +public class HrmEmployeeWorkExperienceController { + + @Resource + private HrmEmployeeWorkExperienceService workExperienceService; + + @GetMapping("/list") + @Operation(summary = "获得员工工作经历列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:query')") + public CommonResult> getWorkExperienceList( + @RequestParam("employeeId") Long employeeId) { + List workExperiences = + workExperienceService.getWorkExperienceListByEmployeeId(employeeId); + return success(BeanUtils.toBean(workExperiences, HrmEmployeeWorkExperienceRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建员工工作经历") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult createWorkExperience( + @Valid @RequestBody HrmEmployeeWorkExperienceSaveReqVO reqVO) { + return success(workExperienceService.createWorkExperience(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新员工工作经历") + @PreAuthorize("@ss.hasPermission('hrm:employee:update')") + public CommonResult updateWorkExperience( + @Valid @RequestBody HrmEmployeeWorkExperienceSaveReqVO reqVO) { + workExperienceService.updateWorkExperience(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除员工工作经历") + @Parameter(name = "id", description = "工作经历编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:employee:delete')") + public CommonResult deleteWorkExperience(@RequestParam("id") Long id) { + workExperienceService.deleteWorkExperience(id); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateRespVO.java new file mode 100644 index 0000000..25ba685 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工证书 Response VO") +@Data +public class HrmEmployeeCertificateRespVO { + + @Schema(description = "证书编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "证书名称", example = "高级工程师") + private String name; + + @Schema(description = "证书级别", example = "高级") + private String level; + + @Schema(description = "证书编号", example = "CERT-001") + private String no; + + @Schema(description = "有效起始日期") + private LocalDateTime startTime; + + @Schema(description = "有效结束日期") + private LocalDateTime endTime; + + @Schema(description = "发证机构", example = "人社局") + private String issuingAuthority; + + @Schema(description = "发证日期") + private LocalDateTime issuingTime; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateSaveReqVO.java new file mode 100644 index 0000000..79b48ed --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/certificate/HrmEmployeeCertificateSaveReqVO.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual; + +@Schema(description = "管理后台 - HRM 员工证书保存 Request VO") +@Data +public class HrmEmployeeCertificateSaveReqVO { + + @Schema(description = "证书编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "证书名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "高级工程师") + @NotBlank(message = "证书名称不能为空") + @Size(max = 255, message = "证书名称长度不能超过 255 个字符") + @DiffLogField(name = "证书名称") + private String name; + + @Schema(description = "证书级别", example = "高级") + @Size(max = 255, message = "证书级别长度不能超过 255 个字符") + @DiffLogField(name = "证书级别") + private String level; + + @Schema(description = "证书编号", example = "CERT-001") + @Size(max = 255, message = "证书编号长度不能超过 255 个字符") + @DiffLogField(name = "证书编号") + private String no; + + @Schema(description = "有效起始日期") + @DiffLogField(name = "有效起始日期") + private LocalDateTime startTime; + + @Schema(description = "有效结束日期") + @DiffLogField(name = "有效结束日期") + private LocalDateTime endTime; + + @Schema(description = "发证机构", example = "人社局") + @Size(max = 255, message = "发证机构长度不能超过 255 个字符") + @DiffLogField(name = "发证机构") + private String issuingAuthority; + + @Schema(description = "发证日期") + @DiffLogField(name = "发证日期") + private LocalDateTime issuingTime; + + @Schema(description = "备注", example = "专业技术资格证书") + @Size(max = 500, message = "备注长度不能超过 500 个字符") + @DiffLogField(name = "备注") + private String remark; + + @Schema(description = "排序", example = "1") + @DiffLogField(name = "排序") + private Integer sort; + + @AssertTrue(message = "证书有效期的结束日期不能早于开始日期") + @JsonIgnore + public boolean isTimeRangeValid() { + return startTime == null || endTime == null || isAfterOrEqual(endTime, startTime); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordCreateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordCreateReqVO.java new file mode 100644 index 0000000..0c514c8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordCreateReqVO.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工异动记录创建 Request VO") +@Data +public class HrmEmployeeChangeRecordCreateReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "异动类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "异动类型不能为空") + @InEnum(value = HrmEmployeeChangeTypeEnum.class, message = "异动类型必须是 {value}") + private Integer type; + + @Schema(description = "异动原因", example = "1") + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}") + private Integer reason; + + @Schema(description = "原部门编号", example = "100") + private Long oldDeptId; + + @Schema(description = "原岗位名称", example = "Java 工程师") + @Size(max = 255, message = "原岗位名称不能超过 255 个字符") + private String oldPostName; + + @Schema(description = "原职级", example = "P5") + @Size(max = 255, message = "原职级不能超过 255 个字符") + private String oldPostLevel; + + @Schema(description = "原工作地点", example = "杭州") + @Size(max = 255, message = "原工作地点不能超过 255 个字符") + private String oldWorkAddress; + + @Schema(description = "原直属上级员工编号", example = "2") + private Long oldLeaderEmployeeId; + + @Schema(description = "新部门编号", example = "200") + private Long newDeptId; + + @Schema(description = "新岗位名称", example = "高级 Java 工程师") + @Size(max = 255, message = "新岗位名称不能超过 255 个字符") + private String newPostName; + + @Schema(description = "新职级", example = "P6") + @Size(max = 255, message = "新职级不能超过 255 个字符") + private String newPostLevel; + + @Schema(description = "新工作地点", example = "上海") + @Size(max = 255, message = "新工作地点不能超过 255 个字符") + private String newWorkAddress; + + @Schema(description = "新直属上级员工编号", example = "3") + private Long newLeaderEmployeeId; + + @Schema(description = "试用期,单位月", example = "3") + @Min(value = 0, message = "试用期不能小于 0") + @Max(value = 6, message = "试用期不能超过 6 个月") + private Integer probation; + + @Schema(description = "生效时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "生效时间不能为空") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "年度晋升") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordRespVO.java new file mode 100644 index 0000000..5c747a1 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeChangeRecordRespVO.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工异动记录 Response VO") +@Data +public class HrmEmployeeChangeRecordRespVO { + + @Schema(description = "异动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "变动类型", example = "5") + private Integer type; + + @Schema(description = "异动原因", example = "1") + private Integer reason; + + @Schema(description = "原部门编号", example = "100") + private Long oldDeptId; + + @Schema(description = "原部门名称", example = "研发部") + private String oldDeptName; + + @Schema(description = "新部门编号", example = "200") + private Long newDeptId; + + @Schema(description = "新部门名称", example = "产品部") + private String newDeptName; + + @Schema(description = "原岗位名称") + private String oldPostName; + + @Schema(description = "新岗位名称") + private String newPostName; + + @Schema(description = "原职级") + private String oldPostLevel; + + @Schema(description = "新职级") + private String newPostLevel; + + @Schema(description = "原工作地点") + private String oldWorkAddress; + + @Schema(description = "新工作地点") + private String newWorkAddress; + + @Schema(description = "原直属上级员工编号") + private Long oldLeaderEmployeeId; + + @Schema(description = "原直属上级员工姓名") + private String oldLeaderEmployeeName; + + @Schema(description = "新直属上级员工编号") + private Long newLeaderEmployeeId; + + @Schema(description = "新直属上级员工姓名") + private String newLeaderEmployeeName; + + @Schema(description = "试用期,单位月") + private Integer probation; + + @Schema(description = "生效日期") + private LocalDateTime effectTime; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeConvertToFullTimeReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeConvertToFullTimeReqVO.java new file mode 100644 index 0000000..1907443 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeConvertToFullTimeReqVO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工转为全职 Request VO") +@Data +public class HrmEmployeeConvertToFullTimeReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "异动原因不能为空") + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}") + private Integer reason; + + @Schema(description = "试用期,单位:月;0 表示无试用期", requiredMode = Schema.RequiredMode.REQUIRED, + example = "3") + @NotNull(message = "试用期不能为空") + @Min(value = 0, message = "试用期不能小于 0") + @Max(value = 6, message = "试用期不能超过 6 个月") + private Integer probation; + + @Schema(description = "新部门编号;未填写表示不变", example = "200") + private Long newDeptId; + + @Schema(description = "新岗位名称;未填写表示不变", example = "Java 工程师") + @Size(max = 255, message = "新岗位名称不能超过 255 个字符") + private String newPostName; + + @Schema(description = "新职级;未填写表示不变", example = "P6") + @Size(max = 255, message = "新职级不能超过 255 个字符") + private String newPostLevel; + + @Schema(description = "新工作地点;未填写表示不变", example = "上海") + @Size(max = 255, message = "新工作地点不能超过 255 个字符") + private String newWorkAddress; + + @Schema(description = "新直属上级员工编号;未填写表示不变", example = "3") + private Long newLeaderEmployeeId; + + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "生效日期不能为空") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "转为全职员工") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeDemoteReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeDemoteReqVO.java new file mode 100644 index 0000000..0442e0e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeDemoteReqVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工降级 Request VO") +@Data +public class HrmEmployeeDemoteReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "异动原因不能为空") + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}") + private Integer reason; + + @Schema(description = "新部门编号", example = "200") + private Long newDeptId; + + @Schema(description = "新岗位名称", example = "Java 工程师") + @Size(max = 255, message = "新岗位名称不能超过 255 个字符") + private String newPostName; + + @Schema(description = "新职级", example = "P5") + @Size(max = 255, message = "新职级不能超过 255 个字符") + private String newPostLevel; + + @Schema(description = "新工作地点", example = "杭州") + @Size(max = 255, message = "新工作地点不能超过 255 个字符") + private String newWorkAddress; + + @Schema(description = "新直属上级员工编号", example = "3") + private Long newLeaderEmployeeId; + + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "生效日期不能为空") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "岗位职级调整") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @AssertTrue(message = "降级原因不合法") + @JsonIgnore + public boolean isReasonValid() { + return reason == null || HrmEmployeeChangeReasonEnum.DEMOTION_REASONS.contains(reason); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeePromoteReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeePromoteReqVO.java new file mode 100644 index 0000000..3b2b1c6 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeePromoteReqVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工晋升 Request VO") +@Data +public class HrmEmployeePromoteReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "异动原因不能为空") + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}") + private Integer reason; + + @Schema(description = "新部门编号", example = "200") + private Long newDeptId; + + @Schema(description = "新岗位名称", example = "高级 Java 工程师") + @Size(max = 255, message = "新岗位名称不能超过 255 个字符") + private String newPostName; + + @Schema(description = "新职级", example = "P6") + @Size(max = 255, message = "新职级不能超过 255 个字符") + private String newPostLevel; + + @Schema(description = "新工作地点", example = "上海") + @Size(max = 255, message = "新工作地点不能超过 255 个字符") + private String newWorkAddress; + + @Schema(description = "新直属上级员工编号", example = "3") + private Long newLeaderEmployeeId; + + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "生效日期不能为空") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "年度晋升") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @AssertTrue(message = "晋升原因不合法") + @JsonIgnore + public boolean isReasonValid() { + return reason == null || HrmEmployeeChangeReasonEnum.TRANSFER_AND_PROMOTION_REASONS.contains(reason); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeRegularReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeRegularReqVO.java new file mode 100644 index 0000000..86d6c1f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeRegularReqVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工转正 Request VO") +@Data +public class HrmEmployeeRegularReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "异动原因不能为空") + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}") + private Integer reason; + + @Schema(description = "新部门编号;未填写表示不变", example = "200") + private Long newDeptId; + + @Schema(description = "新岗位名称;未填写表示不变", example = "Java 工程师") + @Size(max = 255, message = "新岗位名称不能超过 255 个字符") + private String newPostName; + + @Schema(description = "新职级;未填写表示不变", example = "P6") + @Size(max = 255, message = "新职级不能超过 255 个字符") + private String newPostLevel; + + @Schema(description = "新工作地点;未填写表示不变", example = "上海") + @Size(max = 255, message = "新工作地点不能超过 255 个字符") + private String newWorkAddress; + + @Schema(description = "新直属上级员工编号;未填写表示不变", example = "3") + private Long newLeaderEmployeeId; + + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "生效日期不能为空") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "试用期考核通过") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeTransferReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeTransferReqVO.java new file mode 100644 index 0000000..71ef61a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/changerecord/HrmEmployeeTransferReqVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.changerecord; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工调岗 Request VO") +@Data +public class HrmEmployeeTransferReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "异动原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "异动原因不能为空") + @InEnum(value = HrmEmployeeChangeReasonEnum.class, message = "异动原因必须是 {value}") + private Integer reason; + + @Schema(description = "新部门编号", example = "200") + private Long newDeptId; + + @Schema(description = "新岗位名称", example = "产品经理") + @Size(max = 255, message = "新岗位名称不能超过 255 个字符") + private String newPostName; + + @Schema(description = "新职级", example = "P6") + @Size(max = 255, message = "新职级不能超过 255 个字符") + private String newPostLevel; + + @Schema(description = "新工作地点", example = "上海") + @Size(max = 255, message = "新工作地点不能超过 255 个字符") + private String newWorkAddress; + + @Schema(description = "新直属上级员工编号", example = "3") + private Long newLeaderEmployeeId; + + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "生效日期不能为空") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "业务调整") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @AssertTrue(message = "调岗原因不合法") + @JsonIgnore + public boolean isReasonValid() { + return reason == null || HrmEmployeeChangeReasonEnum.TRANSFER_AND_PROMOTION_REASONS.contains(reason); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeArchiveFieldConfigSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeArchiveFieldConfigSaveReqVO.java new file mode 100644 index 0000000..d6ffbf1 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeArchiveFieldConfigSaveReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工档案字段配置保存 Request VO") +@Data +public class HrmEmployeeArchiveFieldConfigSaveReqVO { + + @Schema(description = "字段配置列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "字段配置列表不能为空") + @DiffLogField(name = "字段配置") + private List fields; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeCreateFieldConfigSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeCreateFieldConfigSaveReqVO.java new file mode 100644 index 0000000..f45f17f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeCreateFieldConfigSaveReqVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 新建员工字段配置保存 Request VO") +@Data +public class HrmEmployeeCreateFieldConfigSaveReqVO { + + @Schema(description = "入职状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "入职状态不能为空") + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}") + @Range(min = 1, max = 2, message = "入职状态必须是 1 或 2") + @DiffLogField(name = "入职状态") + private Integer entryStatus; + + @Schema(description = "字段配置列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "字段配置列表不能为空") + @DiffLogField(name = "字段配置") + private List fields; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigRespVO.java new file mode 100644 index 0000000..cef80ed --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工字段配置 Response VO") +@Data +public class HrmEmployeeFieldConfigRespVO { + + @Schema(description = "字段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "name") + private String name; + + @Schema(description = "字段标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "员工姓名") + private String title; + + @Schema(description = "字段分组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "基本信息") + private String groupName; + + @Schema(description = "是否显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean visible; + + @Schema(description = "是否允许员工编辑", example = "true") + private Boolean editable; + + @Schema(description = "是否锁定显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean visibleLocked; + + @Schema(description = "是否锁定编辑", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean editableLocked; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigSaveReqVO.java new file mode 100644 index 0000000..d7e773c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigSaveReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工字段配置保存 Request VO") +@Data +public class HrmEmployeeFieldConfigSaveReqVO { + + @Schema(description = "字段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "name") + @NotBlank(message = "字段名称不能为空") + @Size(max = 40, message = "字段名称不能超过 40 个字符") + @DiffLogField(name = "字段名称") + private String name; + + @Schema(description = "是否显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否显示不能为空") + @DiffLogField(name = "是否显示") + private Boolean visible; + + @Schema(description = "是否允许员工编辑", example = "true") + @DiffLogField(name = "是否允许员工编辑") + private Boolean editable; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigValueVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigValueVO.java new file mode 100644 index 0000000..3b1c88b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/config/HrmEmployeeFieldConfigValueVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - HRM 员工字段配置值 VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeFieldConfigValueVO { + + @Schema(description = "字段名称", example = "mobile") + private String name; + + @Schema(description = "是否显示", example = "true") + private Boolean visible; + + @Schema(description = "是否允许员工编辑", example = "true") + private Boolean editable; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactRespVO.java new file mode 100644 index 0000000..6b0c2f0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工联系人 Response VO") +@Data +public class HrmEmployeeContactRespVO { + + @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "联系人名称", example = "李四") + private String name; + + @Schema(description = "关系", example = "配偶") + private String relation; + + @Schema(description = "联系人电话", example = "15601691301") + private String phone; + + @Schema(description = "联系人工作单位") + private String workUnit; + + @Schema(description = "联系人职务") + private String postName; + + @Schema(description = "联系人地址") + private String address; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactSaveReqVO.java new file mode 100644 index 0000000..46e8cd8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contact/HrmEmployeeContactSaveReqVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工联系人保存 Request VO") +@Data +public class HrmEmployeeContactSaveReqVO { + + @Schema(description = "联系人编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "联系人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotBlank(message = "联系人名称不能为空") + @Size(max = 64, message = "联系人名称长度不能超过 64 个字符") + @DiffLogField(name = "联系人名称") + private String name; + + @Schema(description = "关系", example = "配偶") + @Size(max = 64, message = "关系长度不能超过 64 个字符") + @DiffLogField(name = "关系") + private String relation; + + @Schema(description = "联系人电话", example = "15601691301") + @Size(max = 40, message = "联系人电话长度不能超过 40 个字符") + @DiffLogField(name = "联系人电话") + private String phone; + + @Schema(description = "联系人工作单位", example = "示例科技") + @Size(max = 128, message = "联系人工作单位长度不能超过 128 个字符") + @DiffLogField(name = "联系人工作单位") + private String workUnit; + + @Schema(description = "联系人职务", example = "产品经理") + @Size(max = 128, message = "联系人职务长度不能超过 128 个字符") + @DiffLogField(name = "联系人职务") + private String postName; + + @Schema(description = "联系人地址", example = "杭州市西湖区") + @Size(max = 255, message = "联系人地址长度不能超过 255 个字符") + @DiffLogField(name = "联系人地址") + private String address; + + @Schema(description = "排序", example = "1") + @DiffLogField(name = "排序") + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractRespVO.java new file mode 100644 index 0000000..cb4265d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractRespVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工合同 Response VO") +@Data +public class HrmEmployeeContractRespVO { + + @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "合同编号", example = "HT-2026-001") + private String no; + + @Schema(description = "合同类型", example = "1") + private Integer type; + + @Schema(description = "合同开始日期") + private LocalDateTime startTime; + + @Schema(description = "合同结束日期") + private LocalDateTime endTime; + + @Schema(description = "期限", example = "36") + private Integer term; + + @Schema(description = "合同状态", example = "1") + private Integer status; + + @Schema(description = "签约公司") + private String signCompany; + + @Schema(description = "合同签订日期") + private LocalDateTime signTime; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否到期提醒", example = "true") + private Boolean expireRemind; + + @Schema(description = "附件地址数组") + private List fileUrls; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractSaveReqVO.java new file mode 100644 index 0000000..d2c60ed --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/contract/HrmEmployeeContractSaveReqVO.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contract; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeContractStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeContractTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual; + +@Schema(description = "管理后台 - HRM 员工合同保存 Request VO") +@Data +public class HrmEmployeeContractSaveReqVO { + + @Schema(description = "合同编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "合同编号", example = "HT-2026-001") + @Size(max = 128, message = "合同编号长度不能超过 128 个字符") + @DiffLogField(name = "合同编号") + private String no; + + @Schema(description = "合同类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "合同类型不能为空") + @InEnum(value = HrmEmployeeContractTypeEnum.class, message = "合同类型必须是 {value}") + @DiffLogField(name = "合同类型") + private Integer type; + + @Schema(description = "合同开始日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "合同开始日期不能为空") + @DiffLogField(name = "合同开始日期") + private LocalDateTime startTime; + + @Schema(description = "合同结束日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "合同结束日期不能为空") + @DiffLogField(name = "合同结束日期") + private LocalDateTime endTime; + + @Schema(description = "期限,单位:年", example = "3") + @Min(value = 1, message = "合同期限不能小于 1 年") + @Max(value = 10, message = "合同期限不能超过 10 年") + @DiffLogField(name = "合同期限") + private Integer term; + + @Schema(description = "合同状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "合同状态不能为空") + @InEnum(value = HrmEmployeeContractStatusEnum.class, message = "合同状态必须是 {value}") + @DiffLogField(name = "合同状态") + private Integer status; + + @Schema(description = "签约公司", example = "示例科技有限公司") + @Size(max = 255, message = "签约公司长度不能超过 255 个字符") + @DiffLogField(name = "签约公司") + private String signCompany; + + @Schema(description = "合同签订日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "合同签订日期不能为空") + @DiffLogField(name = "合同签订日期") + private LocalDateTime signTime; + + @Schema(description = "备注", example = "首次签订劳动合同") + @Size(max = 500, message = "备注长度不能超过 500 个字符") + @DiffLogField(name = "备注") + private String remark; + + @Schema(description = "是否到期提醒", example = "true") + @DiffLogField(name = "是否到期提醒") + private Boolean expireRemind; + + @Schema(description = "附件地址数组", example = "[https://example.com/contract.pdf]") + @DiffLogField(name = "附件") + private List fileUrls; + + @Schema(description = "排序", example = "1") + @DiffLogField(name = "排序") + private Integer sort; + + @AssertTrue(message = "合同结束日期不能早于开始日期") + @JsonIgnore + public boolean isTimeRangeValid() { + return startTime == null || endTime == null || isAfterOrEqual(endTime, startTime); + } + + @AssertTrue(message = "合同期限不能为空") + @JsonIgnore + public boolean isTermValid() { + return type == null + || HrmEmployeeContractTypeEnum.NON_FIXED_TERM_LABOR_CONTRACT.getType().equals(type) + || term != null; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceRespVO.java new file mode 100644 index 0000000..7efd972 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceRespVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工教育经历 Response VO") +@Data +public class HrmEmployeeEducationExperienceRespVO { + + @Schema(description = "教育经历编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "学历", example = "8") + private Integer education; + + @Schema(description = "毕业院校", example = "浙江大学") + private String graduateSchool; + + @Schema(description = "专业", example = "软件工程") + private String major; + + @Schema(description = "入学日期") + private LocalDateTime admissionTime; + + @Schema(description = "毕业日期") + private LocalDateTime graduationTime; + + @Schema(description = "教学方式", example = "1") + private Integer teachingMethods; + + @Schema(description = "是否第一学历", example = "true") + private Boolean firstDegree; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceSaveReqVO.java new file mode 100644 index 0000000..aa40592 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/educationexperience/HrmEmployeeEducationExperienceSaveReqVO.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual; + +@Schema(description = "管理后台 - HRM 员工教育经历保存 Request VO") +@Data +public class HrmEmployeeEducationExperienceSaveReqVO { + + @Schema(description = "教育经历编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "学历", requiredMode = Schema.RequiredMode.REQUIRED, example = "8") + @NotNull(message = "学历不能为空") + @DiffLogField(name = "学历") + private Integer education; + + @Schema(description = "毕业院校", requiredMode = Schema.RequiredMode.REQUIRED, example = "浙江大学") + @NotBlank(message = "毕业院校不能为空") + @Size(max = 255, message = "毕业院校长度不能超过 255 个字符") + @DiffLogField(name = "毕业院校") + private String graduateSchool; + + @Schema(description = "专业", requiredMode = Schema.RequiredMode.REQUIRED, example = "软件工程") + @NotBlank(message = "专业不能为空") + @Size(max = 255, message = "专业长度不能超过 255 个字符") + @DiffLogField(name = "专业") + private String major; + + @Schema(description = "入学日期") + @DiffLogField(name = "入学日期") + private LocalDateTime admissionTime; + + @Schema(description = "毕业日期") + @DiffLogField(name = "毕业日期") + private LocalDateTime graduationTime; + + @Schema(description = "教学方式", example = "1") + @DiffLogField(name = "教学方式") + private Integer teachingMethods; + + @Schema(description = "是否第一学历", example = "true") + @DiffLogField(name = "是否第一学历") + private Boolean firstDegree; + + @Schema(description = "排序", example = "1") + @DiffLogField(name = "排序") + private Integer sort; + + @AssertTrue(message = "毕业日期不能早于入学日期") + @JsonIgnore + public boolean isTimeRangeValid() { + return admissionTime == null || graduationTime == null + || isAfterOrEqual(graduationTime, admissionTime); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCancelQuitReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCancelQuitReqVO.java new file mode 100644 index 0000000..97e13e3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCancelQuitReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工取消离职 Request VO") +@Data +public class HrmEmployeeCancelQuitReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "员工撤回离职申请") + @NotBlank(message = "取消原因不能为空") + @Size(max = 500, message = "取消原因不能超过 500 个字符") + private String reason; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeConfirmEntryReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeConfirmEntryReqVO.java new file mode 100644 index 0000000..ec9a6b8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeConfirmEntryReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeOrEqualNow; + +@Schema(description = "管理后台 - HRM 员工确认入职 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmEmployeeConfirmEntryReqVO extends HrmEmployeeSaveReqVO { + + @AssertTrue(message = "入职时间不能晚于当前时间") + @JsonIgnore + public boolean isEntryTimeValid() { + return getEntryTime() == null || beforeOrEqualNow(getEntryTime()); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCreateFromUserReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCreateFromUserReqVO.java new file mode 100644 index 0000000..2413090 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeCreateFromUserReqVO.java @@ -0,0 +1,101 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 从后台用户创建员工 Request VO") +@Data +public class HrmEmployeeCreateFromUserReqVO { + + @Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "后台用户编号不能为空") + private Long userId; + + @Schema(description = "工号", requiredMode = Schema.RequiredMode.REQUIRED, example = "HRM001") + @NotBlank(message = "工号不能为空") + @Size(max = 64, message = "工号不能超过 64 个字符") + private String jobNumber; + + @Schema(description = "员工手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotBlank(message = "员工手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "直属上级员工编号", example = "1") + private Long leaderEmployeeId; + + @Schema(description = "聘用形式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "聘用形式不能为空") + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}") + private Integer type; + + @Schema(description = "员工状态;非正式员工必填", example = "3") + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}") + private Integer status; + + @Schema(description = "入职时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "入职时间不能为空") + private LocalDateTime entryTime; + + @Schema(description = "试用期,单位:月;正式员工必填", example = "3") + @Min(value = 0, message = "试用期不能小于 0") + @Max(value = 6, message = "试用期不能超过 6 个月") + private Integer probation; + + @Schema(description = "职位名称", example = "Java 工程师") + @Size(max = 255, message = "职位名称不能超过 255 个字符") + private String postName; + + @Schema(description = "岗位职级", example = "P6") + @Size(max = 255, message = "岗位职级不能超过 255 个字符") + private String postLevel; + + @Schema(description = "工作城市", example = "杭州") + @Size(max = 64, message = "工作城市不能超过 64 个字符") + private String workCity; + + @Schema(description = "工作地点", example = "西湖区") + @Size(max = 255, message = "工作地点不能超过 255 个字符") + private String workAddress; + + @Schema(description = "备注", example = "从后台用户批量建档") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @AssertTrue(message = "正式员工试用期不能为空") + @JsonIgnore + public boolean isProbationValid() { + return ObjUtil.notEqual(HrmEmployeeTypeEnum.FORMAL.getType(), type) || probation != null; + } + + @AssertTrue(message = "非正式员工状态不能为空") + @JsonIgnore + public boolean isStatusRequired() { + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type) || status != null; + } + + @AssertTrue(message = "员工状态与聘用形式不匹配") + @JsonIgnore + public boolean isStatusValid() { + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type) + || status == null || HrmEmployeeStatusEnum.INFORMAL_STATUSES.contains(status); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeDeptStatisticsRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeDeptStatisticsRespVO.java new file mode 100644 index 0000000..7533fb7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeDeptStatisticsRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工部门统计 Response VO") +@Data +@AllArgsConstructor +public class HrmEmployeeDeptStatisticsRespVO { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long deptId; + + @Schema(description = "在职员工人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long activeCount; + + @Schema(description = "全职员工人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8") + private Long fullTimeCount; + + @Schema(description = "非全职员工人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Long nonFullTimeCount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportExcelVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportExcelVO.java new file mode 100644 index 0000000..87b37db --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportExcelVO.java @@ -0,0 +1,210 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.framework.excel.core.HrmEmployeeProbationExcelColumnSelectFunction; +import cn.iocoder.yudao.module.hrm.framework.excel.core.HrmRecruitChannelExcelColumnSelectFunction; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工档案 Excel 导入 VO") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ExcelIgnoreUnannotated +public class HrmEmployeeImportExcelVO { + + @ExcelProperty("员工姓名") + @Schema(description = "员工姓名") + private String name; + + @ExcelProperty("工号") + @Schema(description = "工号") + private String jobNumber; + + @ExcelProperty("手机号") + @Schema(description = "手机号") + private String mobile; + + @ExcelProperty("国家或地区") + @Schema(description = "国家或地区") + private String country; + + @ExcelProperty("民族") + @Schema(description = "民族") + private String nation; + + @ExcelProperty(value = "证件类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_ID_TYPE) + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_ID_TYPE) + @Schema(description = "证件类型") + private Integer idType; + + @ExcelProperty("证件号码") + @Schema(description = "证件号码") + private String idNumber; + + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat(cn.iocoder.yudao.module.system.enums.DictTypeConstants.USER_SEX) + @ExcelColumnSelect(dictType = cn.iocoder.yudao.module.system.enums.DictTypeConstants.USER_SEX) + @Schema(description = "性别") + private Integer sex; + + @ExcelProperty("邮箱") + @Schema(description = "邮箱") + private String email; + + @ExcelProperty("籍贯") + @Schema(description = "籍贯") + private String nativePlace; + + @ExcelProperty("出生日期") + @Schema(description = "出生日期") + private LocalDateTime birthday; + + @ExcelProperty("户籍地址") + @Schema(description = "户籍地址") + private String address; + + @ExcelProperty(value = "最高学历", converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_EDUCATION) + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_EDUCATION) + @Schema(description = "最高学历") + private Integer highestEducation; + + @ExcelProperty("部门编号") + @Schema(description = "部门编号") + private Long deptId; + + @ExcelProperty("直属上级工号") + @Schema(description = "直属上级工号") + private String leaderJobNumber; + + @ExcelProperty("职位名称") + @Schema(description = "职位名称") + private String postName; + + @ExcelProperty("岗位职级") + @Schema(description = "岗位职级") + private String postLevel; + + @ExcelProperty(value = "入职状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_ENTRY_STATUS) + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_ENTRY_STATUS) + @Schema(description = "入职状态") + private Integer entryStatus; + + @ExcelProperty(value = "员工状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_STATUS) + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_STATUS) + @Schema(description = "员工状态") + private Integer status; + + @ExcelProperty(value = "聘用形式", converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_TYPE) + @ExcelColumnSelect(dictType = DictTypeConstants.HRM_EMPLOYEE_TYPE) + @Schema(description = "聘用形式") + private Integer type; + + @ExcelProperty("入职时间") + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + @ExcelProperty("试用期(月)") + @ExcelColumnSelect(functionName = HrmEmployeeProbationExcelColumnSelectFunction.NAME) + @Schema(description = "试用期(月)") + private Integer probation; + + @ExcelProperty("转正时间") + @Schema(description = "转正时间") + private LocalDateTime regularTime; + + @ExcelProperty("离职时间") + @Schema(description = "离职时间") + private LocalDateTime leaveTime; + + @ExcelProperty("工作城市") + @Schema(description = "工作城市") + private String workCity; + + @ExcelProperty("工作地点") + @Schema(description = "工作地点") + private String workAddress; + + @ExcelProperty("工作详细地址") + @Schema(description = "工作详细地址") + private String workDetailAddress; + + @ExcelProperty("招聘渠道") + @ExcelColumnSelect(functionName = HrmRecruitChannelExcelColumnSelectFunction.NAME) + @Schema(description = "招聘渠道选项,格式为名称(ID)") + private String channelName; + + @ExcelProperty("司龄起算时间") + @Schema(description = "司龄起算时间") + private LocalDateTime companyAgeStartTime; + + @ExcelProperty("绑定用户手机号") + @Schema(description = "绑定后台用户的手机号") + private String userMobile; + + @ExcelProperty("银行卡号") + @Schema(description = "银行卡号") + private String bankCardNumber; + + @ExcelProperty(value = "开户地区", converter = AreaConvert.class) + @Schema(description = "开户地区") + private Integer bankAreaId; + + @ExcelProperty("银行名称") + @Schema(description = "银行名称") + private String bankName; + + @ExcelProperty("开户支行名称") + @Schema(description = "开户支行名称") + private String bankBranchName; + + @ExcelProperty(value = "是否首次参保", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + @ExcelColumnSelect(dictType = DictTypeConstants.BOOLEAN_STRING) + @Schema(description = "是否首次参保") + private Boolean firstSocialSecurity; + + @ExcelProperty(value = "是否首次缴纳公积金", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + @ExcelColumnSelect(dictType = DictTypeConstants.BOOLEAN_STRING) + @Schema(description = "是否首次缴纳公积金") + private Boolean firstAccumulationFund; + + @ExcelProperty("个人社保账号") + @Schema(description = "个人社保账号") + private String socialSecurityNumber; + + @ExcelProperty("个人公积金账号") + @Schema(description = "个人公积金账号") + private String accumulationFundNumber; + + @ExcelProperty("社保起缴月份") + @Schema(description = "社保起缴月份") + private LocalDateTime socialSecurityStartMonth; + + @ExcelProperty("参保方案名称") + @Schema(description = "参保方案名称") + private String schemeName; + + @ExcelProperty("备注") + @Schema(description = "备注") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportRespVO.java new file mode 100644 index 0000000..a555dc0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportRespVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - HRM 员工档案导入 Response VO") +@Data +@Builder +public class HrmEmployeeImportRespVO { + + @Schema(description = "创建成功的工号数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List createJobNumbers; + + @Schema(description = "更新成功的工号数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List updateJobNumbers; + + @Schema(description = "因重复而跳过的工号数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List skipJobNumbers; + + @Schema(description = "导入失败的员工集合,key 为原始行号和员工标识,value 为失败原因", + requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureJobNumbers; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeListReqVO.java new file mode 100644 index 0000000..0688961 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeListReqVO.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusTabEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeSurveyTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTodoTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 员工档案列表 Request VO") +@Data +public class HrmEmployeeListReqVO { + + @Schema(description = "员工编号列表", example = "1,2") + private List ids; + + @Schema(hidden = true) + private List excludeIds; + + @Schema(description = "员工姓名或工号,模糊匹配", example = "张三") + private String search; + + @Schema(description = "员工姓名,模糊匹配", example = "张") + private String name; + + @Schema(description = "工号,模糊匹配", example = "HRM") + private String jobNumber; + + @Schema(description = "手机号,模糊匹配", example = "156") + private String mobile; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门编号数组", example = "100,101") + private List deptIds; + + @Schema(description = "直属上级员工编号", example = "1") + private Long leaderEmployeeId; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "入职时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] entryTime; + + @Schema(description = "职位名称,模糊匹配", example = "Java") + private String postName; + + @Schema(description = "转正时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] regularTime; + + @Schema(description = "工作地点,模糊匹配", example = "西湖区") + private String workAddress; + + @Schema(description = "招聘渠道编号", example = "10") + private Long channelId; + + @Schema(description = "聘用形式", example = "1") + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}") + private Integer type; + + @Schema(description = "入职状态", example = "1") + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}") + private Integer entryStatus; + + @Schema(description = "员工状态", example = "1") + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}") + private Integer status; + + @Schema(description = "员工状态分类", example = "12") + @InEnum(value = HrmEmployeeStatusTabEnum.class, message = "员工状态分类必须是 {value}") + private Integer statusCategory; + + @Schema(description = "首页人事概况筛选类型", example = "1") + @InEnum(value = HrmEmployeeSurveyTypeEnum.class, message = "首页人事概况筛选类型必须是 {value}") + private Integer surveyType; + + @Schema(description = "首页待办筛选类型", example = "5") + @InEnum(value = HrmEmployeeTodoTypeEnum.class, message = "首页待办筛选类型必须是 {value}") + private Integer todoType; + + @Schema(hidden = true) + private LocalDateTime[] activeTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeNotifyRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeNotifyRespVO.java new file mode 100644 index 0000000..c54ba74 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeNotifyRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +@Schema(description = "管理后台 - HRM 员工通知发送结果 Response VO") +@Data +@Accessors(chain = true) +public class HrmEmployeeNotifyRespVO { + + @Schema(description = "发送成功数量", example = "10") + private Integer successCount; + + @Schema(description = "无后台账号跳过数量", example = "2") + private Integer skippedCount; + + @Schema(description = "发送失败数量", example = "1") + private Integer failureCount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeePageReqVO.java new file mode 100644 index 0000000..b69b7e7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeePageReqVO.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusTabEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeSurveyTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTodoTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 员工档案分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmEmployeePageReqVO extends SortablePageParam { + + @Schema(description = "员工编号列表", example = "1,2") + private List ids; + + @Schema(hidden = true) + private List excludeIds; + + @Schema(description = "员工姓名、工号或手机号,模糊匹配", example = "张三") + private String search; + + @Schema(description = "员工姓名,模糊匹配", example = "张") + private String name; + + @Schema(description = "工号,模糊匹配", example = "HRM") + private String jobNumber; + + @Schema(description = "手机号,模糊匹配", example = "156") + private String mobile; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门编号数组", example = "100,101") + private List deptIds; + + @Schema(description = "直属上级员工编号", example = "1") + private Long leaderEmployeeId; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "入职时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] entryTime; + + @Schema(description = "职位名称,模糊匹配", example = "Java") + private String postName; + + @Schema(description = "转正时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] regularTime; + + @Schema(description = "工作地点,模糊匹配", example = "西湖区") + private String workAddress; + + @Schema(description = "招聘渠道编号", example = "10") + private Long channelId; + + @Schema(description = "聘用形式", example = "1") + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}") + private Integer type; + + @Schema(description = "入职状态", example = "1") + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}") + private Integer entryStatus; + + @Schema(description = "员工状态", example = "1") + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}") + private Integer status; + + @Schema(description = "员工状态分类", example = "12") + @InEnum(value = HrmEmployeeStatusTabEnum.class, message = "员工状态分类必须是 {value}") + private Integer statusCategory; + + @Schema(description = "首页人事概况筛选类型", example = "1") + @InEnum(value = HrmEmployeeSurveyTypeEnum.class, message = "首页人事概况筛选类型必须是 {value}") + private Integer surveyType; + + @Schema(description = "首页待办筛选类型", example = "5") + @InEnum(value = HrmEmployeeTodoTypeEnum.class, message = "首页待办筛选类型必须是 {value}") + private Integer todoType; + + @Schema(hidden = true) + private LocalDateTime[] activeTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeQuitReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeQuitReqVO.java new file mode 100644 index 0000000..99b0a48 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeQuitReqVO.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitReasonEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isBeforeOrEqual; + +@Schema(description = "管理后台 - HRM 员工离职 Request VO") +@Data +public class HrmEmployeeQuitReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "计划离职时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "计划离职时间不能为空") + private LocalDateTime planQuitTime; + + @Schema(description = "申请离职时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "申请离职时间不能为空") + private LocalDateTime applyQuitTime; + + @Schema(description = "薪资结算时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "薪资结算时间不能为空") + private LocalDateTime salarySettlementTime; + + @Schema(description = "离职类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "离职类型不能为空") + @InEnum(value = HrmEmployeeQuitTypeEnum.class, message = "离职类型必须是 {value}") + private Integer type; + + @Schema(description = "离职原因", example = "1") + @InEnum(value = HrmEmployeeQuitReasonEnum.class, message = "离职原因必须是 {value}") + private Integer reason; + + @Schema(description = "备注", example = "个人原因离职") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @AssertTrue(message = "申请离职时间不能晚于计划离职时间") + @JsonIgnore + public boolean isApplyQuitTimeValid() { + return applyQuitTime == null || planQuitTime == null + || isBeforeOrEqual(applyQuitTime.toLocalDate(), planQuitTime.toLocalDate()); + } + + @AssertTrue(message = "薪资结算时间不能早于计划离职时间") + @JsonIgnore + public boolean isSalarySettlementTimeValid() { + return salarySettlementTime == null || planQuitTime == null + || isAfterOrEqual(salarySettlementTime.toLocalDate(), planQuitTime.toLocalDate()); + } + + @AssertTrue(message = "离职原因与离职类型不匹配") + @JsonIgnore + public boolean isReasonValid() { + if (type == null || HrmEmployeeQuitTypeEnum.RETIREMENT.getType().equals(type)) { + return true; + } + HrmEmployeeQuitReasonEnum quitReason = HrmEmployeeQuitReasonEnum.valueOf(reason); + return quitReason != null && quitReason.getQuitType().equals(type); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRehireReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRehireReqVO.java new file mode 100644 index 0000000..64ace83 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRehireReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeOrEqualNow; + +@Schema(description = "管理后台 - HRM 员工再入职 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmEmployeeRehireReqVO extends HrmEmployeeSaveReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @AssertTrue(message = "入职时间不能晚于当前时间") + @JsonIgnore + public boolean isEntryTimeValid() { + return getEntryTime() == null || beforeOrEqualNow(getEntryTime()); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRespVO.java new file mode 100644 index 0000000..d7e0319 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRespVO.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工档案 Response VO") +@Data +@Accessors(chain = true) +@ExcelIgnoreUnannotated +public class HrmEmployeeRespVO { + + @ExcelProperty(value = "员工编号", index = 0) + @Schema(description = "员工档案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @ExcelProperty(value = "员工姓名", index = 1) + @Schema(description = "员工姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String name; + + @ExcelProperty(value = "工号", index = 2) + @Schema(description = "工号", example = "HRM001") + private String jobNumber; + + @Schema(description = "系统用户编号", example = "1024") + private Long userId; + + @ExcelProperty(value = "绑定用户", index = 17) + @Schema(description = "后台用户昵称", example = "芋道") + private String userNickname; + + @ExcelProperty(value = "手机号", index = 3) + @Schema(description = "手机号", example = "15601691300") + private String mobile; + + @Schema(description = "国家地区", example = "中国") + private String country; + + @Schema(description = "民族", example = "汉族") + private String nation; + + @Schema(description = "证件类型", example = "1") + private Integer idType; + + @Schema(description = "证件号码", example = "310101199001011234") + private String idNumber; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "邮箱", example = "hrm@example.com") + private String email; + + @Schema(description = "籍贯", example = "浙江杭州") + private String nativePlace; + + @Schema(description = "出生时间") + private LocalDateTime birthday; + + @Schema(description = "年龄", example = "28") + private Integer age; + + @Schema(description = "户籍地址", example = "杭州市西湖区") + private String address; + + @Schema(description = "最高学历", example = "8") + private Integer highestEducation; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @ExcelProperty(value = "部门", index = 4) + @Schema(description = "部门名称", example = "研发部") + private String deptName; + + @Schema(description = "直属上级员工编号", example = "1") + private Long leaderEmployeeId; + + @ExcelProperty(value = "直属上级", index = 7) + @Schema(description = "直属上级员工姓名", example = "李四") + private String leaderEmployeeName; + + @ExcelProperty(value = "入职状态", index = 8, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_ENTRY_STATUS) + @Schema(description = "入职状态", example = "1") + private Integer entryStatus; + + @ExcelProperty(value = "员工状态", index = 9, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_STATUS) + @Schema(description = "员工状态", example = "1") + private Integer status; + + @ExcelProperty(value = "聘用形式", index = 10, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_EMPLOYEE_TYPE) + @Schema(description = "聘用形式", example = "1") + private Integer type; + + @ExcelProperty(value = "入职时间", index = 11) + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + @Schema(description = "试用期,单位月;0 表示无试用期", example = "3") + private Integer probation; + + @ExcelProperty(value = "转正时间", index = 12) + @Schema(description = "转正时间") + private LocalDateTime regularTime; + + @ExcelProperty(value = "离职时间", index = 13) + @Schema(description = "离职时间") + private LocalDateTime leaveTime; + + @ExcelProperty(value = "岗位", index = 5) + @Schema(description = "职位名称", example = "Java 工程师") + private String postName; + + @ExcelProperty(value = "岗位职级", index = 6) + @Schema(description = "岗位职级", example = "P6") + private String postLevel; + + @ExcelProperty(value = "工作城市", index = 14) + @Schema(description = "工作城市", example = "上海") + private String workCity; + + @ExcelProperty(value = "工作地点", index = 15) + @Schema(description = "工作地址", example = "浦东新区") + private String workAddress; + + @Schema(description = "工作详细地址", example = "张江高科 1 号楼") + private String workDetailAddress; + + @Schema(description = "招聘渠道编号", example = "10") + private Long channelId; + + @ExcelProperty(value = "招聘渠道", index = 16) + @Schema(description = "招聘渠道名称", example = "BOSS直聘") + private String channelName; + + @Schema(description = "司龄开始时间") + private LocalDateTime companyAgeStartTime; + + @Schema(description = "司龄,单位年", example = "2") + private Integer companyAge; + + @Schema(description = "关联招聘候选人编号", example = "200") + private Long candidateId; + + // ==================== 工资卡信息 ==================== + + @Schema(description = "银行卡号", example = "622202600001") + private String salaryCardNumber; + + @Schema(description = "开户地区编号", example = "440300") + private Integer salaryCardAreaId; + + @Schema(description = "开户地区名称", example = "广东省 深圳市") + private String salaryCardAreaName; + + @Schema(description = "银行名称", example = "招商银行") + private String salaryCardBankName; + + @Schema(description = "开户支行名称", example = "科技园支行") + private String salaryCardBankBranchName; + + // ==================== 社保信息 ==================== + + @Schema(description = "个人社保账号", example = "SB20260001") + private String socialSecurityNumber; + + @Schema(description = "个人公积金账号", example = "GJJ20260001") + private String accumulationFundNumber; + + @Schema(description = "备注", example = "首批迁移员工") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeSaveReqVO.java new file mode 100644 index 0000000..7fcad99 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeSaveReqVO.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.hrm.enums.employee.experience.HrmEmployeeEducationEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeIdTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isBeforeOrEqual; + +@Schema(description = "管理后台 - HRM 员工档案新增/修改 Request VO") +@Data +public class HrmEmployeeSaveReqVO { + + @Schema(description = "员工档案编号", example = "1024") + private Long id; + + @Schema(description = "员工姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotBlank(message = "员工姓名不能为空") + @Size(max = 255, message = "员工姓名不能超过 255 个字符") + private String name; + + @Schema(description = "工号", example = "HRM001") + @Size(max = 64, message = "工号不能超过 64 个字符") + private String jobNumber; + + @Schema(description = "系统用户编号", example = "1024") + private Long userId; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotBlank(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "国家地区", example = "中国") + @Size(max = 64, message = "国家地区不能超过 64 个字符") + private String country; + + @Schema(description = "民族", example = "汉族") + @Size(max = 64, message = "民族不能超过 64 个字符") + private String nation; + + @Schema(description = "证件类型", example = "1") + @InEnum(value = HrmEmployeeIdTypeEnum.class, message = "证件类型必须是 {value}") + private Integer idType; + + @Schema(description = "证件号码", example = "310101199001011234") + @Size(max = 255, message = "证件号码不能超过 255 个字符") + private String idNumber; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "邮箱", example = "hrm@example.com") + @Email(message = "邮箱格式不正确") + @Size(max = 255, message = "邮箱不能超过 255 个字符") + private String email; + + @Schema(description = "籍贯", example = "浙江杭州") + @Size(max = 128, message = "籍贯不能超过 128 个字符") + private String nativePlace; + + @Schema(description = "出生时间") + @PastOrPresent(message = "出生时间不能晚于当前时间") + private LocalDateTime birthday; + + @Schema(description = "年龄", example = "28") + private Integer age; + + @Schema(description = "户籍地址", example = "杭州市西湖区") + @Size(max = 255, message = "户籍地址不能超过 255 个字符") + private String address; + + @Schema(description = "最高学历", example = "8") + @InEnum(value = HrmEmployeeEducationEnum.class, message = "最高学历必须是 {value}") + private Integer highestEducation; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "直属上级员工编号", example = "1") + private Long leaderEmployeeId; + + @Schema(description = "入职状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "入职状态不能为空") + @InEnum(value = HrmEmployeeEntryStatusEnum.class, message = "入职状态必须是 {value}") + private Integer entryStatus; + + @Schema(description = "员工状态", example = "1") + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}") + private Integer status; + + @Schema(description = "聘用形式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "聘用形式不能为空") + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}") + private Integer type; + + @Schema(description = "入职时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "入职时间不能为空") + private LocalDateTime entryTime; + + @Schema(description = "试用期,单位月;0 表示无试用期", example = "3") + @Min(value = 0, message = "试用期不能小于 0 个月") + @Max(value = 6, message = "试用期不能超过 6 个月") + private Integer probation; + + @Schema(description = "转正时间") + private LocalDateTime regularTime; + + @Schema(description = "离职时间") + private LocalDateTime leaveTime; + + @Schema(description = "职位名称", example = "Java 工程师") + @Size(max = 255, message = "职位名称不能超过 255 个字符") + private String postName; + + @Schema(description = "岗位职级", example = "P6") + @Size(max = 255, message = "岗位职级不能超过 255 个字符") + private String postLevel; + + @Schema(description = "工作城市", example = "上海") + @Size(max = 64, message = "工作城市不能超过 64 个字符") + private String workCity; + + @Schema(description = "工作地址", example = "浦东新区") + @Size(max = 255, message = "工作地址不能超过 255 个字符") + private String workAddress; + + @Schema(description = "工作详细地址", example = "张江高科 1 号楼") + @Size(max = 255, message = "工作详细地址不能超过 255 个字符") + private String workDetailAddress; + + @Schema(description = "招聘渠道编号", example = "10") + private Long channelId; + + @Schema(description = "司龄开始时间") + private LocalDateTime companyAgeStartTime; + + @Schema(description = "司龄,单位年", example = "2") + private Integer companyAge; + + @Schema(description = "关联招聘候选人编号", example = "200") + private Long candidateId; + + @Schema(description = "备注", example = "首批迁移员工") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @AssertTrue(message = "身份证号码格式不正确") + @JsonIgnore + public boolean isIdNumberValid() { + return ObjUtil.notEqual(HrmEmployeeIdTypeEnum.ID_CARD.getType(), idType) + || StrUtil.isBlank(idNumber) || IdcardUtil.isValidCard(idNumber); + } + + @AssertTrue(message = "员工状态与聘用形式不匹配") + @JsonIgnore + public boolean isStatusValid() { + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type) + || status == null || HrmEmployeeStatusEnum.INFORMAL_STATUSES.contains(status); + } + + @AssertTrue(message = "非正式员工状态不能为空") + @JsonIgnore + public boolean isStatusRequired() { + return ObjUtil.notEqual(HrmEmployeeTypeEnum.INFORMAL.getType(), type) || status != null; + } + + @AssertTrue(message = "在职员工工号不能为空") + @JsonIgnore + public boolean isJobNumberValid() { + return ObjUtil.notEqual(HrmEmployeeEntryStatusEnum.ACTIVE.getStatus(), entryStatus) + || StrUtil.isNotBlank(jobNumber); + } + + @AssertTrue(message = "正式员工试用期不能为空") + @JsonIgnore + public boolean isProbationValid() { + return ObjUtil.notEqual(HrmEmployeeTypeEnum.FORMAL.getType(), type) || probation != null; + } + + @AssertTrue(message = "司龄起算时间不能晚于入职时间") + @JsonIgnore + public boolean isCompanyAgeStartTimeValid() { + return entryTime == null || companyAgeStartTime == null + || isBeforeOrEqual(companyAgeStartTime, entryTime); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeStatusCountRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeStatusCountRespVO.java new file mode 100644 index 0000000..6cac978 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeStatusCountRespVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工状态数量 Response VO") +@Data +@AllArgsConstructor +public class HrmEmployeeStatusCountRespVO { + + @Schema(description = "状态页签", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Integer status; + + @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long count; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileRespVO.java new file mode 100644 index 0000000..a3e61b2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工材料附件 Response VO") +@Data +public class HrmEmployeeFileRespVO { + + @Schema(description = "附件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long employeeId; + + @Schema(description = "附件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + private Integer type; + + @Schema(description = "附件地址", requiredMode = Schema.RequiredMode.REQUIRED) + private String url; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileSaveReqVO.java new file mode 100644 index 0000000..4c793b6 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/file/HrmEmployeeFileSaveReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.file; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeFileTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工材料附件保存 Request VO") +@Data +public class HrmEmployeeFileSaveReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "附件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + @NotNull(message = "附件类型不能为空") + @InEnum(value = HrmEmployeeFileTypeEnum.class, message = "附件类型必须是 {value}") + private Integer type; + + @Schema(description = "附件地址数组", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[https://example.com/archive.pdf]") + @NotNull(message = "附件地址数组不能为空") + @Size(max = 20, message = "同类附件不能超过 20 个") + private List<@NotBlank(message = "附件地址不能为空") + @Size(max = 512, message = "附件地址长度不能超过 512 个字符") String> fileUrls; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/personalnote/HrmEmployeePersonalNoteCreateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/personalnote/HrmEmployeePersonalNoteCreateReqVO.java new file mode 100644 index 0000000..baf3feb --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/personalnote/HrmEmployeePersonalNoteCreateReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.personalnote; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工个人备忘创建 Request VO") +@Data +public class HrmEmployeePersonalNoteCreateReqVO { + + @Schema(description = "备忘内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "跟进转正材料") + @NotBlank(message = "备忘内容不能为空") + @Size(max = 1024, message = "备忘内容不能超过 1024 个字符") + private String content; + + @Schema(description = "提醒时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "提醒时间不能为空") + private LocalDateTime reminderTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/quitinfo/HrmEmployeeQuitInfoRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/quitinfo/HrmEmployeeQuitInfoRespVO.java new file mode 100644 index 0000000..a7bbcbf --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/quitinfo/HrmEmployeeQuitInfoRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.quitinfo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工离职信息 Response VO") +@Data +public class HrmEmployeeQuitInfoRespVO { + + @Schema(description = "离职信息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "计划离职时间") + private LocalDateTime planQuitTime; + + @Schema(description = "申请离职时间") + private LocalDateTime applyQuitTime; + + @Schema(description = "薪资结算时间") + private LocalDateTime salarySettlementTime; + + @Schema(description = "离职类型", example = "1") + private Integer type; + + @Schema(description = "离职原因", example = "1") + private Integer reason; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "离职前员工状态") + private Integer oldEmployeeStatus; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/quitinfo/HrmEmployeeQuitInfoSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/quitinfo/HrmEmployeeQuitInfoSaveReqVO.java new file mode 100644 index 0000000..ed97170 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/quitinfo/HrmEmployeeQuitInfoSaveReqVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.quitinfo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitReasonEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工离职信息保存 Request VO") +@Data +public class HrmEmployeeQuitInfoSaveReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "计划离职时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "计划离职时间不能为空") + private LocalDateTime planQuitTime; + + @Schema(description = "申请离职时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "申请离职时间不能为空") + private LocalDateTime applyQuitTime; + + @Schema(description = "薪资结算时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "薪资结算时间不能为空") + private LocalDateTime salarySettlementTime; + + @Schema(description = "离职类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "离职类型不能为空") + @InEnum(value = HrmEmployeeQuitTypeEnum.class, message = "离职类型必须是 {value}") + private Integer type; + + @Schema(description = "离职原因", example = "1") + @InEnum(value = HrmEmployeeQuitReasonEnum.class, message = "离职原因必须是 {value}") + private Integer reason; + + @Schema(description = "备注", example = "个人原因离职") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @Schema(description = "离职前员工状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "离职前员工状态不能为空") + @InEnum(value = HrmEmployeeStatusEnum.class, message = "离职前员工状态必须是 {value}") + private Integer oldEmployeeStatus; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/salarycard/HrmEmployeeSalaryCardRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/salarycard/HrmEmployeeSalaryCardRespVO.java new file mode 100644 index 0000000..d1da559 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/salarycard/HrmEmployeeSalaryCardRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.salarycard; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工工资卡 Response VO") +@Data +public class HrmEmployeeSalaryCardRespVO { + + @Schema(description = "工资卡编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long employeeId; + + @Schema(description = "银行卡号", example = "622202600001") + private String bankCardNumber; + + @Schema(description = "开户地区编号", example = "440300") + private Integer bankAreaId; + + @Schema(description = "开户地区名称", example = "广东省 深圳市") + private String bankAreaName; + + @Schema(description = "银行名称", example = "招商银行") + private String bankName; + + @Schema(description = "开户支行名称", example = "科技园支行") + private String bankBranchName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/salarycard/HrmEmployeeSalaryCardSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/salarycard/HrmEmployeeSalaryCardSaveReqVO.java new file mode 100644 index 0000000..e0125bc --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/salarycard/HrmEmployeeSalaryCardSaveReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.salarycard; + +import cn.iocoder.yudao.module.hrm.framework.operatelog.core.HrmBankCardNumberParseFunction; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工工资卡保存 Request VO") +@Data +public class HrmEmployeeSalaryCardSaveReqVO { + + @Schema(description = "工资卡编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工不能为空") + private Long employeeId; + + @Schema(description = "银行卡号", requiredMode = Schema.RequiredMode.REQUIRED, example = "622202600001") + @NotBlank(message = "银行卡号不能为空") + @Size(max = 64, message = "银行卡号不能超过 64 个字符") + @DiffLogField(name = "银行卡号", function = HrmBankCardNumberParseFunction.NAME) + private String bankCardNumber; + + @Schema(description = "开户地区编号", example = "440300") + @DiffLogField(name = "开户地区编号") + private Integer bankAreaId; + + @Schema(description = "银行名称", example = "招商银行") + @Size(max = 64, message = "银行名称不能超过 64 个字符") + @DiffLogField(name = "银行名称") + private String bankName; + + @Schema(description = "开户支行名称", example = "科技园支行") + @Size(max = 128, message = "开户支行名称不能超过 128 个字符") + @DiffLogField(name = "开户支行名称") + private String bankBranchName; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/trainingexperience/HrmEmployeeTrainingExperienceRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/trainingexperience/HrmEmployeeTrainingExperienceRespVO.java new file mode 100644 index 0000000..ad83c06 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/trainingexperience/HrmEmployeeTrainingExperienceRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.trainingexperience; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工培训经历 Response VO") +@Data +public class HrmEmployeeTrainingExperienceRespVO { + + @Schema(description = "培训经历编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "培训课程", example = "项目管理") + private String course; + + @Schema(description = "培训机构名称", example = "内部学院") + private String organizationName; + + @Schema(description = "培训开始日期") + private LocalDateTime startTime; + + @Schema(description = "培训结束日期") + private LocalDateTime endTime; + + @Schema(description = "培训时长", example = "16 小时") + private String duration; + + @Schema(description = "培训成绩", example = "优秀") + private String result; + + @Schema(description = "培训证书名称", example = "PMP") + private String certificateName; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/trainingexperience/HrmEmployeeTrainingExperienceSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/trainingexperience/HrmEmployeeTrainingExperienceSaveReqVO.java new file mode 100644 index 0000000..72c4804 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/trainingexperience/HrmEmployeeTrainingExperienceSaveReqVO.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.trainingexperience; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual; + +@Schema(description = "管理后台 - HRM 员工培训经历保存 Request VO") +@Data +public class HrmEmployeeTrainingExperienceSaveReqVO { + + @Schema(description = "培训经历编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "培训课程", requiredMode = Schema.RequiredMode.REQUIRED, example = "项目管理") + @NotBlank(message = "培训课程不能为空") + @Size(max = 128, message = "培训课程长度不能超过 128 个字符") + @DiffLogField(name = "培训课程") + private String course; + + @Schema(description = "培训机构名称", example = "内部学院") + @Size(max = 128, message = "培训机构名称长度不能超过 128 个字符") + @DiffLogField(name = "培训机构名称") + private String organizationName; + + @Schema(description = "培训开始日期") + @DiffLogField(name = "培训开始日期") + private LocalDateTime startTime; + + @Schema(description = "培训结束日期") + @DiffLogField(name = "培训结束日期") + private LocalDateTime endTime; + + @Schema(description = "培训时长", example = "16 小时") + @Size(max = 64, message = "培训时长长度不能超过 64 个字符") + @DiffLogField(name = "培训时长") + private String duration; + + @Schema(description = "培训成绩", example = "优秀") + @Size(max = 64, message = "培训成绩长度不能超过 64 个字符") + @DiffLogField(name = "培训成绩") + private String result; + + @Schema(description = "培训证书名称", example = "PMP") + @Size(max = 128, message = "培训证书名称长度不能超过 128 个字符") + @DiffLogField(name = "培训证书名称") + private String certificateName; + + @Schema(description = "备注", example = "公司内部培训") + @Size(max = 500, message = "备注长度不能超过 500 个字符") + @DiffLogField(name = "备注") + private String remark; + + @Schema(description = "排序", example = "1") + @DiffLogField(name = "排序") + private Integer sort; + + @AssertTrue(message = "培训经历的结束日期不能早于开始日期") + @JsonIgnore + public boolean isTimeRangeValid() { + return startTime == null || endTime == null || isAfterOrEqual(endTime, startTime); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/workexperience/HrmEmployeeWorkExperienceRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/workexperience/HrmEmployeeWorkExperienceRespVO.java new file mode 100644 index 0000000..4210305 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/workexperience/HrmEmployeeWorkExperienceRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.workexperience; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工工作经历 Response VO") +@Data +public class HrmEmployeeWorkExperienceRespVO { + + @Schema(description = "工作经历编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long employeeId; + + @Schema(description = "工作单位", example = "示例科技") + private String workUnit; + + @Schema(description = "职务", example = "开发工程师") + private String postName; + + @Schema(description = "工作开始日期") + private LocalDateTime startTime; + + @Schema(description = "工作结束日期") + private LocalDateTime endTime; + + @Schema(description = "离职原因") + private String reason; + + @Schema(description = "证明人") + private String witnessName; + + @Schema(description = "证明人手机号") + private String witnessPhone; + + @Schema(description = "工作备注") + private String remark; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/workexperience/HrmEmployeeWorkExperienceSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/workexperience/HrmEmployeeWorkExperienceSaveReqVO.java new file mode 100644 index 0000000..6c8c017 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/workexperience/HrmEmployeeWorkExperienceSaveReqVO.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.workexperience; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.isAfterOrEqual; + +@Schema(description = "管理后台 - HRM 员工工作经历保存 Request VO") +@Data +public class HrmEmployeeWorkExperienceSaveReqVO { + + @Schema(description = "工作经历编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "工作单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "示例科技") + @NotBlank(message = "工作单位不能为空") + @Size(max = 255, message = "工作单位长度不能超过 255 个字符") + @DiffLogField(name = "工作单位") + private String workUnit; + + @Schema(description = "职务", requiredMode = Schema.RequiredMode.REQUIRED, example = "开发工程师") + @NotBlank(message = "职务不能为空") + @Size(max = 255, message = "职务长度不能超过 255 个字符") + @DiffLogField(name = "职务") + private String postName; + + @Schema(description = "工作开始日期") + @DiffLogField(name = "工作开始日期") + private LocalDateTime startTime; + + @Schema(description = "工作结束日期") + @DiffLogField(name = "工作结束日期") + private LocalDateTime endTime; + + @Schema(description = "离职原因", example = "职业发展") + @Size(max = 1024, message = "离职原因长度不能超过 1024 个字符") + @DiffLogField(name = "离职原因") + private String reason; + + @Schema(description = "证明人", example = "李四") + @Size(max = 255, message = "证明人长度不能超过 255 个字符") + @DiffLogField(name = "证明人") + private String witnessName; + + @Schema(description = "证明人手机号", example = "15601691301") + @Size(max = 32, message = "证明人手机号长度不能超过 32 个字符") + @DiffLogField(name = "证明人手机号") + private String witnessPhone; + + @Schema(description = "工作备注", example = "负责核心业务研发") + @Size(max = 500, message = "工作备注长度不能超过 500 个字符") + @DiffLogField(name = "工作备注") + private String remark; + + @Schema(description = "排序", example = "1") + @DiffLogField(name = "排序") + private Integer sort; + + @AssertTrue(message = "工作经历的结束日期不能早于开始日期") + @JsonIgnore + public boolean isTimeRangeValid() { + return startTime == null || endTime == null || isAfterOrEqual(endTime, startTime); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/HrmHomeController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/HrmHomeController.java new file mode 100644 index 0000000..4e30d3b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/HrmHomeController.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.home; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.hrm.controller.admin.home.vo.HrmHomeCalendarItemRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.home.vo.HrmHrHomeStatisticsRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.home.vo.HrmTeamHomeStatisticsRespVO; +import cn.iocoder.yudao.module.hrm.service.home.HrmHomeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 首页") +@RestController +@RequestMapping("/hrm/home") +@Validated +public class HrmHomeController { + + @Resource + private HrmHomeService homeService; + + @GetMapping("/hr-statistics-summary") + @Operation(summary = "获得 HR 工作台统计") + @PreAuthorize("@ss.hasPermission('hrm:home:hr-query')") + public CommonResult getHrHomeStatisticsSummary() { + return success(homeService.getHrHomeStatisticsSummary()); + } + + @GetMapping("/hr-calendar") + @Operation(summary = "获得 HR 工作台日历") + @Parameters({ + @Parameter(name = "startDate", description = "开始日期", required = true, example = "2026-08-01"), + @Parameter(name = "endDate", description = "结束日期", required = true, example = "2026-08-31") + }) + @PreAuthorize("@ss.hasPermission('hrm:home:hr-query')") + public CommonResult> getHrHomeCalendar( + @RequestParam("startDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam("endDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + return success(homeService.getHrHomeCalendar(getLoginUserId(), startDate, endDate)); + } + + @GetMapping("/team-statistics-summary") + @Operation(summary = "获得团队工作台统计") + @PreAuthorize("@ss.hasPermission('hrm:home:team-query')") + public CommonResult getTeamHomeStatisticsSummary() { + return success(homeService.getTeamHomeStatisticsSummary(getLoginUserId())); + } + + @GetMapping("/team-calendar") + @Operation(summary = "获得团队工作台日历") + @Parameters({ + @Parameter(name = "startDate", description = "开始日期", required = true, example = "2026-08-01"), + @Parameter(name = "endDate", description = "结束日期", required = true, example = "2026-08-31") + }) + @PreAuthorize("@ss.hasPermission('hrm:home:team-query')") + public CommonResult> getTeamHomeCalendar( + @RequestParam("startDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam("endDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + return success(homeService.getTeamHomeCalendar(getLoginUserId(), startDate, endDate)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmHomeCalendarItemRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmHomeCalendarItemRespVO.java new file mode 100644 index 0000000..30c225e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmHomeCalendarItemRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +@Schema(description = "管理后台 - HRM 首页日历事项 Response VO") +@Data +public class HrmHomeCalendarItemRespVO { + + @Schema(description = "员工个人备忘编号") + private Long personalNoteId; + + @Schema(description = "事项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "事项类型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "备忘") + private String typeName; + + @Schema(description = "事项内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String content; + + @Schema(description = "关联业务编号") + private Long typeId; + + @Schema(description = "事项日期", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDate date; + + @Schema(description = "事项时间") + private LocalDateTime eventTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmHrHomeStatisticsRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmHrHomeStatisticsRespVO.java new file mode 100644 index 0000000..d98713a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmHrHomeStatisticsRespVO.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - HRM HR 工作台统计 Response VO") +@Data +public class HrmHrHomeStatisticsRespVO { + + @Schema(description = "人事概况") + private EmployeeSurvey employeeSurvey; + + @Schema(description = "招聘动态") + private RecruitSurvey recruitSurvey; + + @Schema(description = "上月薪资") + private SalarySurvey salarySurvey; + + @Schema(description = "待办提醒") + private TodoSurvey todoSurvey; + + @Schema(description = "管理后台 - HRM HR 工作台人事概况 Response VO") + @Data + public static class EmployeeSurvey { + + @Schema(description = "在职人数") + private Long activeCount; + + @Schema(description = "本月入职人数") + private Long entryThisMonthCount; + + @Schema(description = "本月待入职人数") + private Long pendingEntryThisMonthCount; + + @Schema(description = "本月离职人数") + private Long leaveThisMonthCount; + + @Schema(description = "本月待离职人数") + private Long pendingLeaveThisMonthCount; + + @Schema(description = "本月转正人数") + private Long regularThisMonthCount; + + @Schema(description = "本月调岗人数") + private Long transferThisMonthCount; + + } + + @Schema(description = "管理后台 - HRM HR 工作台招聘动态 Response VO") + @Data + public static class RecruitSurvey { + + @Schema(description = "招聘中职位数量") + private Long recruitingPostCount; + + @Schema(description = "招聘流程中候选人数") + private Long candidateInProcessCount; + + @Schema(description = "待入职人数") + private Long pendingEntryCount; + + @Schema(description = "已入职人数") + private Long joinedCount; + + } + + @Schema(description = "管理后台 - HRM HR 工作台上月薪资 Response VO") + @Data + public static class SalarySurvey { + + @Schema(description = "工资表编号") + private Long monthRecordId; + + @Schema(description = "员工数量") + private Integer employeeCount; + + @Schema(description = "实发工资") + private BigDecimal realPaySalary; + + @Schema(description = "部门薪资占比列表") + private List deptProportions; + + } + + @Schema(description = "管理后台 - HRM HR 工作台部门薪资占比 Response VO") + @Data + public static class SalaryDept { + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "薪资占比") + private BigDecimal proportion; + + @Schema(description = "实发工资") + private BigDecimal totalSalary; + + } + + @Schema(description = "管理后台 - HRM HR 工作台待办提醒 Response VO") + @Data + public static class TodoSurvey { + + @Schema(description = "待入职人数") + private Long toEntryCount; + + @Schema(description = "待离职人数") + private Long toLeaveCount; + + @Schema(description = "合同待到期人数") + private Long toExpireContractCount; + + @Schema(description = "待转正人数") + private Long toRegularCount; + + @Schema(description = "待核算工资表数量") + private Long toSalaryComputeCount; + + @Schema(description = "本月生日员工数量") + private Long toBirthdayCount; + + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmTeamHomeStatisticsRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmTeamHomeStatisticsRespVO.java new file mode 100644 index 0000000..1009c12 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/home/vo/HrmTeamHomeStatisticsRespVO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 团队工作台统计 Response VO") +@Data +public class HrmTeamHomeStatisticsRespVO { + + @Schema(description = "当前主管员工编号", example = "1") + private Long leaderEmployeeId; + + @Schema(description = "我的团队") + private TeamOverview teamOverview; + + @Schema(description = "团队概况") + private TeamSurvey teamSurvey; + + @Schema(description = "管理后台 - HRM 团队工作台我的团队 Response VO") + @Data + public static class TeamOverview { + + @Schema(description = "团队人数", example = "12") + private Long employeeCount; + + @Schema(description = "本月入职人数", example = "2") + private Long entryThisMonthCount; + + @Schema(description = "本月离职人数", example = "1") + private Long leaveThisMonthCount; + + @Schema(description = "本月转正人数", example = "3") + private Long regularThisMonthCount; + + } + + @Schema(description = "管理后台 - HRM 团队工作台团队概况 Response VO") + @Data + public static class TeamSurvey { + + @Schema(description = "员工状态占比") + private List statusAnalysis; + + @Schema(description = "男女性别占比") + private List sexAnalysis; + + @Schema(description = "成员年龄占比") + private List ageAnalysis; + + @Schema(description = "成员司龄占比") + private List companyAgeAnalysis; + + } + + @Schema(description = "管理后台 - HRM 团队工作台统计分析项 Response VO") + @Data + public static class AnalysisItem { + + @Schema(description = "分类类型;null 表示未填写", example = "1") + private Integer type; + + @Schema(description = "数量", example = "8") + private Long count; + + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceEmployeeInfoController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceEmployeeInfoController.java new file mode 100644 index 0000000..1dd3cfd --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceEmployeeInfoController.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.employeeinfo.HrmInsuranceEmployeeInfoRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.employeeinfo.HrmInsuranceEmployeeInfoSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.employeeinfo.HrmInsuranceEmployeeInfoUpdateSchemeReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.employee.HrmInsuranceEmployeeInfoDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeDO; +import cn.iocoder.yudao.module.hrm.service.insurance.employee.HrmInsuranceEmployeeInfoService; +import cn.iocoder.yudao.module.hrm.service.insurance.config.HrmInsuranceSchemeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工参保信息") +@RestController +@RequestMapping("/hrm/insurance/employee-info") +@Validated +public class HrmInsuranceEmployeeInfoController { + + @Resource + private HrmInsuranceEmployeeInfoService insuranceEmployeeInfoService; + @Resource + private HrmInsuranceSchemeService insuranceSchemeService; + + @GetMapping("/get") + @Operation(summary = "获得员工参保信息") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:insurance:employee-info:query')") + public CommonResult getInsuranceEmployeeInfo( + @RequestParam("employeeId") Long employeeId) { + HrmInsuranceEmployeeInfoDO employeeInfo = insuranceEmployeeInfoService + .getInsuranceEmployeeInfoByEmployeeId(employeeId); + HrmInsuranceEmployeeInfoRespVO respVO = BeanUtils.toBean( + employeeInfo, HrmInsuranceEmployeeInfoRespVO.class); + if (respVO != null && respVO.getSchemeId() != null) { + HrmInsuranceSchemeDO scheme = insuranceSchemeService.getScheme(respVO.getSchemeId()); + respVO.setSchemeName(scheme == null ? null : scheme.getName()); + } + return success(respVO); + } + + @PutMapping("/save") + @Operation(summary = "保存员工参保信息") + @PreAuthorize("@ss.hasPermission('hrm:insurance:employee-info:update')") + public CommonResult saveInsuranceEmployeeInfo( + @Valid @RequestBody HrmInsuranceEmployeeInfoSaveReqVO reqVO) { + return success(insuranceEmployeeInfoService.saveInsuranceEmployeeInfo(reqVO)); + } + + @PutMapping("/update-scheme") + @Operation(summary = "更新员工参保方案") + @PreAuthorize("@ss.hasPermission('hrm:insurance:employee-info:update')") + public CommonResult updateEmployeeScheme( + @Valid @RequestBody HrmInsuranceEmployeeInfoUpdateSchemeReqVO reqVO) { + insuranceEmployeeInfoService.updateEmployeeScheme(reqVO.getEmployeeId(), reqVO.getSchemeId()); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceMonthEmployeeRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceMonthEmployeeRecordController.java new file mode 100644 index 0000000..bccb82e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceMonthEmployeeRecordController.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee.HrmInsuranceMonthEmployeeProjectRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee.HrmInsuranceMonthEmployeeRecordCreateListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee.HrmInsuranceMonthEmployeeRecordPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee.HrmInsuranceMonthEmployeeRecordRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee.HrmInsuranceMonthEmployeeRecordStopListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee.HrmInsuranceMonthEmployeeRecordUpdateReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.employee.HrmInsuranceEmployeeInfoDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.monthrecord.HrmInsuranceMonthEmployeeRecordDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeDO; +import cn.iocoder.yudao.module.hrm.enums.insurance.config.HrmInsuranceProjectTypeEnum; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.insurance.config.HrmInsuranceSchemeService; +import cn.iocoder.yudao.module.hrm.service.insurance.employee.HrmInsuranceEmployeeInfoService; +import cn.iocoder.yudao.module.hrm.service.insurance.monthrecord.HrmInsuranceMonthEmployeeRecordService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 员工月度社保") +@RestController +@RequestMapping("/hrm/insurance/month-employee-record") +@Validated +public class HrmInsuranceMonthEmployeeRecordController { + + @Resource + private HrmInsuranceMonthEmployeeRecordService monthEmployeeRecordService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmInsuranceEmployeeInfoService insuranceEmployeeInfoService; + @Resource + private HrmInsuranceSchemeService insuranceSchemeService; + + @Resource + private DeptApi deptApi; + + @GetMapping("/page") + @Operation(summary = "获得员工月度社保分页") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:query')") + public CommonResult> getMonthEmployeeRecordPage( + @Valid HrmInsuranceMonthEmployeeRecordPageReqVO reqVO) { + PageResult pageResult = + monthEmployeeRecordService.getMonthEmployeeRecordPage(reqVO); + return success(new PageResult<>(buildMonthEmployeeRecordRespVOList(pageResult.getList()), + pageResult.getTotal())); + } + + @GetMapping("/get") + @Operation(summary = "获得员工月度社保详情") + @Parameter(name = "id", description = "员工月度社保记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:query')") + public CommonResult getMonthEmployeeRecord(@RequestParam("id") Long id) { + HrmInsuranceMonthEmployeeRecordDO employeeRecord = + monthEmployeeRecordService.getMonthEmployeeRecord(id); + if (employeeRecord == null) { + return success(null); + } + return success(CollUtil.getFirst(buildMonthEmployeeRecordRespVOList( + Collections.singletonList(employeeRecord)))); + } + + @PutMapping("/update") + @Operation(summary = "修改员工月度参保项目") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:update')") + public CommonResult updateMonthEmployeeRecord( + @Valid @RequestBody HrmInsuranceMonthEmployeeRecordUpdateReqVO reqVO) { + monthEmployeeRecordService.updateMonthEmployeeRecord(reqVO); + return success(true); + } + + @PutMapping("/stop-list") + @Operation(summary = "批量停止员工参保") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:update')") + public CommonResult stopMonthEmployeeRecordList( + @Valid @RequestBody HrmInsuranceMonthEmployeeRecordStopListReqVO reqVO) { + monthEmployeeRecordService.stopMonthEmployeeRecordList(reqVO.getIds()); + return success(true); + } + + @PostMapping("/create-list") + @Operation(summary = "添加参保人员") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:update')") + public CommonResult createMonthEmployeeRecordList( + @Valid @RequestBody HrmInsuranceMonthEmployeeRecordCreateListReqVO reqVO) { + monthEmployeeRecordService.createMonthEmployeeRecordList(reqVO); + return success(true); + } + + @GetMapping("/uninsured-employee-list") + @Operation(summary = "获得本月未参保员工") + @Parameter(name = "monthRecordId", description = "社保表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:query')") + public CommonResult> getUninsuredEmployeeList( + @RequestParam("monthRecordId") Long monthRecordId) { + List employees = monthEmployeeRecordService.getUninsuredEmployeeList(monthRecordId); + return success(BeanUtils.toBean(employees, HrmEmployeeRespVO.class)); + } + + // ==================== 拼接 VO ==================== + + private List buildMonthEmployeeRecordRespVOList( + List employeeRecords) { + if (CollUtil.isEmpty(employeeRecords)) { + return Collections.emptyList(); + } + + // 1.1 批量查询员工和部门 + Set employeeIds = convertSet(employeeRecords, HrmInsuranceMonthEmployeeRecordDO::getEmployeeId); + Map employeeMap = employeeService.getEmployeeMap(employeeIds); + Map deptMap = deptApi.getDeptMap(convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId)); + // 1.2 批量查询社保方案和员工社保资料 + Map schemeMap = convertMap(insuranceSchemeService.getSchemeListByIds( + convertSet(employeeRecords, HrmInsuranceMonthEmployeeRecordDO::getSchemeId)), + HrmInsuranceSchemeDO::getId); + Map insuranceEmployeeInfoMap = + insuranceEmployeeInfoService.getInsuranceEmployeeInfoMap(employeeIds); + Map> projectMap = + convertMap(employeeRecords, HrmInsuranceMonthEmployeeRecordDO::getId, + record -> record.getProjects() == null ? Collections.emptyList() : record.getProjects()); + + // 2. 拼接响应 + List respVOs = + BeanUtils.toBean(employeeRecords, HrmInsuranceMonthEmployeeRecordRespVO.class); + for (HrmInsuranceMonthEmployeeRecordRespVO respVO : respVOs) { + MapUtils.findAndThen(employeeMap, respVO.getEmployeeId(), employee -> { + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber()) + .setMobile(employee.getMobile()).setIdNumber(employee.getIdNumber()) + .setSex(employee.getSex()).setAge(employee.getAge()) + .setDeptId(employee.getDeptId()).setPostName(employee.getPostName()) + .setEntryStatus(employee.getEntryStatus()).setEmployeeStatus(employee.getStatus()) + .setEntryTime(employee.getEntryTime()); + MapUtils.findAndThen(deptMap, employee.getDeptId(), + dept -> respVO.setDeptName(dept.getName())); + }); + MapUtils.findAndThen(schemeMap, respVO.getSchemeId(), scheme -> { + respVO.setSchemeName(scheme.getName()).setAreaId(scheme.getAreaId()) + .setAreaName(AreaUtils.format(scheme.getAreaId())) + .setHouseType(scheme.getHouseholdType()).setSchemeType(scheme.getType()); + }); + MapUtils.findAndThen(insuranceEmployeeInfoMap, respVO.getEmployeeId(), insuranceEmployeeInfo -> { + respVO.setSocialSecurityNumber(insuranceEmployeeInfo.getSocialSecurityNumber()) + .setAccumulationFundNumber(insuranceEmployeeInfo.getAccumulationFundNumber()); + }); + List projects = BeanUtils.toBean( + projectMap.getOrDefault(respVO.getId(), Collections.emptyList()), + HrmInsuranceMonthEmployeeProjectRespVO.class); + respVO.setSocialSecurityProjectList(convertList(projects, project -> project, + project -> HrmInsuranceProjectTypeEnum.isSocialSecurity(project.getType()))); + respVO.setProvidentFundProjectList(convertList(projects, project -> project, + project -> HrmInsuranceProjectTypeEnum.isProvidentFund(project.getType()))); + } + return respVOs; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceMonthRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceMonthRecordController.java new file mode 100644 index 0000000..4d984a3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceMonthRecordController.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.HrmInsuranceMonthRecordCreateReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.HrmInsuranceMonthRecordRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.monthrecord.HrmInsuranceMonthRecordDO; +import cn.iocoder.yudao.module.hrm.service.insurance.monthrecord.HrmInsuranceMonthRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 月度社保表") +@RestController +@RequestMapping("/hrm/insurance/month-record") +@Validated +public class HrmInsuranceMonthRecordController { + + @Resource + private HrmInsuranceMonthRecordService insuranceMonthRecordService; + + @PostMapping("/create-first") + @Operation(summary = "创建首月社保表") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:create')") + public CommonResult createFirstMonthRecord( + @Valid @RequestBody HrmInsuranceMonthRecordCreateReqVO reqVO) { + return success(insuranceMonthRecordService.createFirstMonthRecord(reqVO)); + } + + @PostMapping("/create-next") + @Operation(summary = "创建下月社保表") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:create')") + public CommonResult createNextMonthRecord() { + return success(insuranceMonthRecordService.createNextMonthRecord()); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除月度社保表") + @Parameter(name = "id", description = "社保表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:delete')") + public CommonResult deleteMonthRecord(@RequestParam("id") Long id) { + insuranceMonthRecordService.deleteMonthRecord(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得月度社保表") + @Parameter(name = "id", description = "月度社保表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:query')") + public CommonResult getMonthRecord(@RequestParam("id") Long id) { + HrmInsuranceMonthRecordDO monthRecord = insuranceMonthRecordService.getMonthRecord(id); + return success(BeanUtils.toBean(monthRecord, HrmInsuranceMonthRecordRespVO.class)); + } + + @GetMapping("/last") + @Operation(summary = "获得最近月度社保表") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:query')") + public CommonResult getLastMonthRecord() { + HrmInsuranceMonthRecordDO monthRecord = insuranceMonthRecordService.getLastMonthRecord(); + return success(BeanUtils.toBean(monthRecord, HrmInsuranceMonthRecordRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得月度社保表列表") + @Parameter(name = "year", description = "年份", example = "2026") + @PreAuthorize("@ss.hasPermission('hrm:insurance:month-record:query')") + public CommonResult> getMonthRecordList( + @RequestParam(value = "year", required = false) Integer year) { + List monthRecords = insuranceMonthRecordService.getMonthRecordList(year); + return success(BeanUtils.toBean(monthRecords, HrmInsuranceMonthRecordRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceSchemeController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceSchemeController.java new file mode 100644 index 0000000..67ada40 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceSchemeController.java @@ -0,0 +1,157 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.scheme.HrmInsuranceSchemeProjectRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.scheme.HrmInsuranceSchemeRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.scheme.HrmInsuranceSchemeSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeProjectDO; +import cn.iocoder.yudao.module.hrm.enums.insurance.config.HrmInsuranceProjectTypeEnum; +import cn.iocoder.yudao.module.hrm.service.insurance.config.HrmInsuranceSchemeService; +import cn.iocoder.yudao.module.hrm.service.insurance.employee.HrmInsuranceEmployeeInfoService; +import cn.iocoder.yudao.module.hrm.service.insurance.monthrecord.HrmInsuranceMonthEmployeeRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; + +@Tag(name = "管理后台 - HRM 社保方案") +@RestController +@RequestMapping("/hrm/insurance/scheme") +@Validated +public class HrmInsuranceSchemeController { + + @Resource + private HrmInsuranceSchemeService insuranceSchemeService; + @Resource + private HrmInsuranceEmployeeInfoService insuranceEmployeeInfoService; + @Resource + private HrmInsuranceMonthEmployeeRecordService insuranceMonthEmployeeRecordService; + + @PostMapping("/create") + @Operation(summary = "创建社保方案") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:create')") + public CommonResult createScheme(@Valid @RequestBody HrmInsuranceSchemeSaveReqVO reqVO) { + return success(insuranceSchemeService.createScheme(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新社保方案") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:update')") + public CommonResult updateScheme(@Valid @RequestBody HrmInsuranceSchemeSaveReqVO reqVO) { + insuranceSchemeService.updateScheme(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除社保方案") + @Parameter(name = "id", description = "方案编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:delete')") + public CommonResult deleteScheme(@RequestParam("id") Long id) { + insuranceSchemeService.deleteScheme(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得社保方案") + @Parameter(name = "id", description = "社保方案编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:query')") + public CommonResult getScheme(@RequestParam("id") Long id) { + HrmInsuranceSchemeDO scheme = insuranceSchemeService.getScheme(id); + return success(buildSchemeRespVO(scheme)); + } + + @GetMapping("/list") + @Operation(summary = "获得社保方案列表") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:query')") + public CommonResult> getSchemeList() { + List schemes = insuranceSchemeService.getSchemeList(); + return success(buildSchemeRespVOList(schemes)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得社保方案精简列表") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:query')") + public CommonResult> getSchemeSimpleList() { + List list = insuranceSchemeService.getSchemeList(); + return success(BeanUtils.toBean(list, HrmInsuranceSchemeRespVO.class, + scheme -> scheme.setAreaName(AreaUtils.format(scheme.getAreaId())))); + } + + // ==================== 拼接 VO ==================== + + private HrmInsuranceSchemeRespVO buildSchemeRespVO(HrmInsuranceSchemeDO scheme) { + if (scheme == null) { + return null; + } + return CollUtil.getFirst(buildSchemeRespVOList(Collections.singletonList(scheme))); + } + + private List buildSchemeRespVOList(List schemes) { + if (CollUtil.isEmpty(schemes)) { + return Collections.emptyList(); + } + // 1. 批量查询社保项目和使用人数 + Set schemeIds = convertSet(schemes, HrmInsuranceSchemeDO::getId); + Map> projectMap = + insuranceSchemeService.getSchemeProjectListMap(schemeIds); + Map useCountMap = + insuranceEmployeeInfoService.getInsuranceEmployeeInfoCountMapBySchemeIds(schemeIds); + Map monthRecordCountMap = insuranceMonthEmployeeRecordService + .getMonthEmployeeRecordCountMapBySchemeIds(schemeIds); + + // 2. 拼接响应 + return convertList(schemes, scheme -> { + HrmInsuranceSchemeRespVO respVO = BeanUtils.toBean(scheme, HrmInsuranceSchemeRespVO.class); + respVO.setAreaName(AreaUtils.format(respVO.getAreaId())); + List projects = BeanUtils.toBean( + projectMap.getOrDefault(scheme.getId(), Collections.emptyList()), HrmInsuranceSchemeProjectRespVO.class); + respVO.setProjectList(projects); + List socialSecurityProjects = convertList( + projects, project -> project, + project -> HrmInsuranceProjectTypeEnum.isSocialSecurity(project.getType())); + List providentFundProjects = convertList( + projects, project -> project, + project -> HrmInsuranceProjectTypeEnum.isProvidentFund(project.getType())); + respVO.setSocialSecurityProjectList(socialSecurityProjects) + .setProvidentFundProjectList(providentFundProjects); + respVO.setPersonalInsuranceAmount(getSumValue(socialSecurityProjects, + HrmInsuranceSchemeProjectRespVO::getPersonalAmount, BigDecimal::add, BigDecimal.ZERO)) + .setCorporateInsuranceAmount(getSumValue(socialSecurityProjects, + HrmInsuranceSchemeProjectRespVO::getCorporateAmount, BigDecimal::add, BigDecimal.ZERO)) + .setPersonalProvidentFundAmount(getSumValue(providentFundProjects, + HrmInsuranceSchemeProjectRespVO::getPersonalAmount, BigDecimal::add, BigDecimal.ZERO)) + .setCorporateProvidentFundAmount(getSumValue(providentFundProjects, + HrmInsuranceSchemeProjectRespVO::getCorporateAmount, BigDecimal::add, BigDecimal.ZERO)) + .setUseCount(useCountMap.getOrDefault(scheme.getId(), 0L)) + .setMonthRecordCount(monthRecordCountMap.getOrDefault(scheme.getId(), 0L)); + return respVO; + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceStandardController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceStandardController.java new file mode 100644 index 0000000..07680d5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/HrmInsuranceStandardController.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard.HrmInsuranceStandardProjectListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard.HrmInsuranceStandardProjectRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard.HrmInsuranceStandardTypeListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard.HrmInsuranceStandardTypeRespVO; +import cn.iocoder.yudao.module.hrm.service.insurance.config.HrmInsuranceStandardService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 标准参保数据") +@RestController +@RequestMapping("/hrm/insurance/standard") +@Validated +public class HrmInsuranceStandardController { + + @Resource + private HrmInsuranceStandardService insuranceStandardService; + + @GetMapping("/type-list") + @Operation(summary = "获得标准参保类型列表") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:query')") + public CommonResult> getStandardTypeList( + @Valid HrmInsuranceStandardTypeListReqVO reqVO) { + return success(insuranceStandardService.getStandardTypeList(reqVO.getAreaId())); + } + + @GetMapping("/project-list") + @Operation(summary = "获得标准参保项目列表") + @PreAuthorize("@ss.hasPermission('hrm:insurance:scheme:query')") + public CommonResult> getStandardProjectList( + @Valid HrmInsuranceStandardProjectListReqVO reqVO) { + return success(insuranceStandardService.getStandardProjectList(reqVO.getAreaId(), reqVO.getTypeCode())); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoRespVO.java new file mode 100644 index 0000000..4c5d094 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.employeeinfo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工参保信息 Response VO") +@Data +public class HrmInsuranceEmployeeInfoRespVO { + + @Schema(description = "员工参保信息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long employeeId; + + @Schema(description = "是否本地首次缴纳社保", example = "true") + private Boolean firstSocialSecurity; + + @Schema(description = "是否本地首次缴纳公积金", example = "false") + private Boolean firstAccumulationFund; + + @Schema(description = "社保账号", example = "SB20260001") + private String socialSecurityNumber; + + @Schema(description = "公积金账号", example = "GJJ20260001") + private String accumulationFundNumber; + + @Schema(description = "社保起缴月份") + private LocalDateTime socialSecurityStartMonth; + + @Schema(description = "社保方案编号", example = "1024") + private Long schemeId; + + @Schema(description = "社保方案名称", example = "深圳标准社保方案") + private String schemeName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoSaveReqVO.java new file mode 100644 index 0000000..fea4f46 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoSaveReqVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.employeeinfo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工参保信息保存 Request VO") +@Data +public class HrmInsuranceEmployeeInfoSaveReqVO { + + @Schema(description = "员工参保信息编号", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工不能为空") + private Long employeeId; + + @Schema(description = "是否本地首次缴纳社保", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否本地首次缴纳社保不能为空") + private Boolean firstSocialSecurity; + + @Schema(description = "是否本地首次缴纳公积金", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否本地首次缴纳公积金不能为空") + private Boolean firstAccumulationFund; + + @Schema(description = "社保账号", example = "SB20260001") + @Size(max = 64, message = "社保账号不能超过 64 个字符") + private String socialSecurityNumber; + + @Schema(description = "公积金账号", example = "GJJ20260001") + @Size(max = 64, message = "公积金账号不能超过 64 个字符") + private String accumulationFundNumber; + + @Schema(description = "社保起缴月份") + private LocalDateTime socialSecurityStartMonth; + + @Schema(description = "社保方案编号", example = "1024") + private Long schemeId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoUpdateSchemeReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoUpdateSchemeReqVO.java new file mode 100644 index 0000000..28a87cd --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/employeeinfo/HrmInsuranceEmployeeInfoUpdateSchemeReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.employeeinfo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工参保方案更新 Request VO") +@Data +public class HrmInsuranceEmployeeInfoUpdateSchemeReqVO { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工不能为空") + private Long employeeId; + + @Schema(description = "社保方案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "社保方案不能为空") + private Long schemeId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/HrmInsuranceMonthRecordCreateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/HrmInsuranceMonthRecordCreateReqVO.java new file mode 100644 index 0000000..77ff671 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/HrmInsuranceMonthRecordCreateReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 月度社保表创建 Request VO") +@Data +public class HrmInsuranceMonthRecordCreateReqVO { + + @Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + @NotNull(message = "年份不能为空") + @Min(value = 2000, message = "年份不能小于 2000") + @Max(value = 2100, message = "年份不能大于 2100") + private Integer year; + + @Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + @NotNull(message = "月份不能为空") + @Min(value = 1, message = "月份必须在 1 到 12 之间") + @Max(value = 12, message = "月份必须在 1 到 12 之间") + private Integer month; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/HrmInsuranceMonthRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/HrmInsuranceMonthRecordRespVO.java new file mode 100644 index 0000000..3f3a452 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/HrmInsuranceMonthRecordRespVO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 月度社保表 Response VO") +@Data +public class HrmInsuranceMonthRecordRespVO { + + @Schema(description = "月度社保表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + private Integer year; + + @Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + private Integer month; + + @Schema(description = "标题", example = "2026 年 7 月社保表") + private String title; + + @Schema(description = "参保人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer insuredEmployeeCount; + + @Schema(description = "停保人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer stoppedEmployeeCount; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "个人社保金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "800.00") + private BigDecimal personalInsuranceAmount; + + @Schema(description = "个人公积金金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "700.00") + private BigDecimal personalProvidentFundAmount; + + @Schema(description = "企业社保金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1600.00") + private BigDecimal corporateInsuranceAmount; + + @Schema(description = "企业公积金金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "700.00") + private BigDecimal corporateProvidentFundAmount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeProjectRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeProjectRespVO.java new file mode 100644 index 0000000..bd63e12 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeProjectRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工月度社保项目 Response VO") +@Data +public class HrmInsuranceMonthEmployeeProjectRespVO { + + @Schema(description = "来源社保方案项目编号", example = "1024") + private Long schemeProjectId; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "养老保险") + private String name; + + @Schema(description = "缴纳基数", example = "10000.00") + private BigDecimal baseAmount; + + @Schema(description = "公司缴纳比例", example = "16.00") + private BigDecimal corporateRate; + + @Schema(description = "个人缴纳比例", example = "8.00") + private BigDecimal personalRate; + + @Schema(description = "公司缴纳金额", example = "1600.00") + private BigDecimal corporateAmount; + + @Schema(description = "个人缴纳金额", example = "800.00") + private BigDecimal personalAmount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeProjectUpdateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeProjectUpdateReqVO.java new file mode 100644 index 0000000..140a611 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeProjectUpdateReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工月度社保项目调整 Request VO") +@Data +public class HrmInsuranceMonthEmployeeProjectUpdateReqVO { + + @Schema(description = "社保方案项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "社保方案项目编号不能为空") + private Long schemeProjectId; + + @Schema(description = "缴纳基数", example = "10000.00") + @DecimalMin(value = "0", message = "缴纳基数不能小于 0") + @Digits(integer = 10, fraction = 2, message = "缴纳基数最多 10 位整数和 2 位小数") + private BigDecimal baseAmount; + + @Schema(description = "公司缴纳金额", example = "1600.00") + @DecimalMin(value = "0", message = "公司缴纳金额不能小于 0") + @Digits(integer = 10, fraction = 2, message = "公司缴纳金额最多 10 位整数和 2 位小数") + private BigDecimal corporateAmount; + + @Schema(description = "个人缴纳金额", example = "800.00") + @DecimalMin(value = "0", message = "个人缴纳金额不能小于 0") + @Digits(integer = 10, fraction = 2, message = "个人缴纳金额最多 10 位整数和 2 位小数") + private BigDecimal personalAmount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordCreateListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordCreateListReqVO.java new file mode 100644 index 0000000..9fad291 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordCreateListReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 添加参保人员 Request VO") +@Data +public class HrmInsuranceMonthEmployeeRecordCreateListReqVO { + + @Schema(description = "月度社保表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "社保表编号不能为空") + private Long monthRecordId; + + @Schema(description = "员工编号列表", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[1024, 1025]") + @NotEmpty(message = "员工不能为空") + private List<@NotNull(message = "员工编号不能为空") Long> employeeIds; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordPageReqVO.java new file mode 100644 index 0000000..89acdf1 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordPageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.insurance.employee.HrmInsuranceEmployeeStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 员工月度社保分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmInsuranceMonthEmployeeRecordPageReqVO extends PageParam { + + @Schema(description = "月度社保表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "社保表编号不能为空") + private Long monthRecordId; + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "员工姓名", example = "张三") + private String employeeName; + + @Schema(description = "社保方案编号", example = "1024") + private Long schemeId; + + @Schema(description = "参保地区编号", example = "440300") + private Integer areaId; + + @Schema(description = "参保状态", example = "1") + @InEnum(value = HrmInsuranceEmployeeStatusEnum.class, message = "参保状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordRespVO.java new file mode 100644 index 0000000..fe05804 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordRespVO.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工月度社保 Response VO") +@Data +public class HrmInsuranceMonthEmployeeRecordRespVO { + + @Schema(description = "记录编号") + private Long id; + + @Schema(description = "社保表编号") + private Long monthRecordId; + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "员工姓名") + private String employeeName; + + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "手机号") + private String mobile; + + @Schema(description = "身份证号") + private String idNumber; + + @Schema(description = "性别") + private Integer sex; + + @Schema(description = "年龄") + private Integer age; + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门") + private String deptName; + + @Schema(description = "职位名称") + private String postName; + + @Schema(description = "入职状态") + private Integer entryStatus; + + @Schema(description = "员工状态") + private Integer employeeStatus; + + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + @Schema(description = "社保方案编号") + private Long schemeId; + + @Schema(description = "社保方案") + private String schemeName; + + @Schema(description = "参保地区") + private String areaName; + + @Schema(description = "参保地区编号") + private Integer areaId; + + @Schema(description = "户籍类型") + private String houseType; + + @Schema(description = "社保方案类型") + private Integer schemeType; + + @Schema(description = "个人社保号") + private String socialSecurityNumber; + + @Schema(description = "个人公积金号") + private String accumulationFundNumber; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "个人社保") + private BigDecimal personalInsuranceAmount; + + @Schema(description = "个人公积金") + private BigDecimal personalProvidentFundAmount; + + @Schema(description = "公司社保") + private BigDecimal corporateInsuranceAmount; + + @Schema(description = "公司公积金") + private BigDecimal corporateProvidentFundAmount; + + @Schema(description = "参保状态") + private Integer status; + + @Schema(description = "社保项目列表") + private List socialSecurityProjectList; + + @Schema(description = "公积金项目列表") + private List providentFundProjectList; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordStopListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordStopListReqVO.java new file mode 100644 index 0000000..78d6b59 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordStopListReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工停止参保 Request VO") +@Data +public class HrmInsuranceMonthEmployeeRecordStopListReqVO { + + @Schema(description = "员工月度社保记录编号列表", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[1024, 1025]") + @NotEmpty(message = "员工社保记录不能为空") + private List<@NotNull(message = "员工社保记录编号不能为空") Long> ids; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordUpdateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordUpdateReqVO.java new file mode 100644 index 0000000..266e002 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/monthrecord/employee/HrmInsuranceMonthEmployeeRecordUpdateReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.monthrecord.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工月度社保项目修改 Request VO") +@Data +public class HrmInsuranceMonthEmployeeRecordUpdateReqVO { + + @Schema(description = "员工月度社保记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工社保记录编号不能为空") + private Long id; + + @Schema(description = "社保方案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "社保方案不能为空") + private Long schemeId; + + @Schema(description = "社保项目列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "社保项目不能为空") + @Valid + private List projects; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeProjectRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeProjectRespVO.java new file mode 100644 index 0000000..ac10171 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeProjectRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.scheme; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 社保方案项目 Response VO") +@Data +public class HrmInsuranceSchemeProjectRespVO { + + @Schema(description = "社保方案项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "社保方案编号", example = "1024") + private Long schemeId; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "养老保险") + private String name; + + @Schema(description = "缴纳基数", example = "10000.00") + private BigDecimal baseAmount; + + @Schema(description = "公司缴纳比例", example = "16.00") + private BigDecimal corporateRate; + + @Schema(description = "个人缴纳比例", example = "8.00") + private BigDecimal personalRate; + + @Schema(description = "公司缴纳金额", example = "1600.00") + private BigDecimal corporateAmount; + + @Schema(description = "个人缴纳金额", example = "800.00") + private BigDecimal personalAmount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeProjectSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeProjectSaveReqVO.java new file mode 100644 index 0000000..9dbc9f7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeProjectSaveReqVO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.scheme; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.insurance.config.HrmInsuranceProjectTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 社保方案项目保存 Request VO") +@Data +public class HrmInsuranceSchemeProjectSaveReqVO { + + @Schema(description = "社保方案项目编号", example = "1024") + private Long id; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "项目类型不能为空") + @InEnum(value = HrmInsuranceProjectTypeEnum.class, message = "项目类型必须是 {value}") + private Integer type; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "养老保险") + @NotBlank(message = "项目名称不能为空") + @Size(max = 64, message = "项目名称不能超过 64 个字符") + private String name; + + @Schema(description = "缴纳基数", example = "10000.00") + @DecimalMin(value = "0.00", message = "缴纳基数不能小于 0") + @Digits(integer = 10, fraction = 2, message = "缴纳基数最多 10 位整数和 2 位小数") + private BigDecimal baseAmount; + + @Schema(description = "公司缴纳比例", example = "16.00") + @DecimalMin(value = "0.00", message = "公司缴纳比例不能小于 0") + @DecimalMax(value = "100.00", message = "公司缴纳比例不能大于 100") + @Digits(integer = 3, fraction = 2, message = "公司缴纳比例最多 2 位小数") + private BigDecimal corporateRate; + + @Schema(description = "个人缴纳比例", example = "8.00") + @DecimalMin(value = "0.00", message = "个人缴纳比例不能小于 0") + @DecimalMax(value = "100.00", message = "个人缴纳比例不能大于 100") + @Digits(integer = 3, fraction = 2, message = "个人缴纳比例最多 2 位小数") + private BigDecimal personalRate; + + @Schema(description = "公司缴纳金额", example = "1600.00") + @DecimalMin(value = "0.00", message = "公司缴纳金额不能小于 0") + @Digits(integer = 10, fraction = 2, message = "公司缴纳金额最多 10 位整数和 2 位小数") + private BigDecimal corporateAmount; + + @Schema(description = "个人缴纳金额", example = "800.00") + @DecimalMin(value = "0.00", message = "个人缴纳金额不能小于 0") + @Digits(integer = 10, fraction = 2, message = "个人缴纳金额最多 10 位整数和 2 位小数") + private BigDecimal personalAmount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeRespVO.java new file mode 100644 index 0000000..3c8e71b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeRespVO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.scheme; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 社保方案 Response VO") +@Data +public class HrmInsuranceSchemeRespVO { + + @Schema(description = "社保方案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "方案名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "深圳标准社保方案") + private String name; + + @Schema(description = "参保地区编号", example = "440300") + private Integer areaId; + + @Schema(description = "参保地区", example = "广东省 深圳市") + private String areaName; + + @Schema(description = "户籍类型", example = "深户") + private String householdType; + + @Schema(description = "方案类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "全部社保项目") + private List projectList; + + @Schema(description = "社保项目") + private List socialSecurityProjectList; + + @Schema(description = "公积金项目") + private List providentFundProjectList; + + @Schema(description = "个人社保金额", example = "800.00") + private BigDecimal personalInsuranceAmount; + + @Schema(description = "公司社保金额", example = "1600.00") + private BigDecimal corporateInsuranceAmount; + + @Schema(description = "个人公积金金额", example = "700.00") + private BigDecimal personalProvidentFundAmount; + + @Schema(description = "公司公积金金额", example = "700.00") + private BigDecimal corporateProvidentFundAmount; + + @Schema(description = "使用人数", example = "10") + private Long useCount; + + @Schema(description = "历史月记录数", example = "120") + private Long monthRecordCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeSaveReqVO.java new file mode 100644 index 0000000..d2f4661 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/scheme/HrmInsuranceSchemeSaveReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.scheme; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.insurance.config.HrmInsuranceSchemeTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 社保方案保存 Request VO") +@Data +public class HrmInsuranceSchemeSaveReqVO { + + @Schema(description = "社保方案编号", example = "1024") + private Long id; + + @Schema(description = "方案名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "深圳标准社保方案") + @NotBlank(message = "方案名称不能为空") + @Size(max = 64, message = "方案名称不能超过 64 个字符") + private String name; + + @Schema(description = "参保地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "440300") + @NotNull(message = "参保城市不能为空") + private Integer areaId; + + @Schema(description = "户籍类型", example = "深户") + @Size(max = 64, message = "户籍类型不能超过 64 个字符") + private String householdType; + + @Schema(description = "方案类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "方案类型不能为空") + @InEnum(value = HrmInsuranceSchemeTypeEnum.class, message = "方案类型必须是 {value}") + private Integer type; + + @Schema(description = "社保项目列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "社保项目不能为空") + private List<@NotNull(message = "社保项目不能为空") @Valid HrmInsuranceSchemeProjectSaveReqVO> projectList; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardProjectListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardProjectListReqVO.java new file mode 100644 index 0000000..64a7909 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardProjectListReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 标准参保项目列表 Request VO") +@Data +public class HrmInsuranceStandardProjectListReqVO { + + @Schema(description = "参保地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "440300") + @NotNull(message = "参保城市不能为空") + private Integer areaId; + + @Schema(description = "参保方案编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SS100173") + @NotBlank(message = "参保方案编码不能为空") + private String typeCode; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardProjectRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardProjectRespVO.java new file mode 100644 index 0000000..a75887a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardProjectRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 标准参保项目 Response VO") +@Data +public class HrmInsuranceStandardProjectRespVO { + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "养老保险") + private String name; + + @Schema(description = "缴纳基数", example = "10000.00") + private BigDecimal baseAmount; + + @Schema(description = "公司缴纳比例", example = "16.00") + private BigDecimal corporateRate; + + @Schema(description = "个人缴纳比例", example = "8.00") + private BigDecimal personalRate; + + @Schema(description = "公司缴纳金额", example = "1600.00") + private BigDecimal corporateAmount; + + @Schema(description = "个人缴纳金额", example = "800.00") + private BigDecimal personalAmount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardTypeListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardTypeListReqVO.java new file mode 100644 index 0000000..1a907fd --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardTypeListReqVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 标准参保类型列表 Request VO") +@Data +public class HrmInsuranceStandardTypeListReqVO { + + @Schema(description = "参保地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "440300") + @NotNull(message = "参保城市不能为空") + private Integer areaId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardTypeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardTypeRespVO.java new file mode 100644 index 0000000..aaf9d3c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/insurance/vo/standard/HrmInsuranceStandardTypeRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.insurance.vo.standard; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 标准参保类型 Response VO") +@Data +public class HrmInsuranceStandardTypeRespVO { + + @Schema(description = "参保方案编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SS100173") + private String code; + + @Schema(description = "参保方案名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "深户一档") + private String name; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/HrmOperateLogController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/HrmOperateLogController.java new file mode 100644 index 0000000..b353cdd --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/HrmOperateLogController.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.operatelog; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.operatelog.vo.HrmOperateLogPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.operatelog.vo.HrmOperateLogRespVO; +import cn.iocoder.yudao.module.hrm.enums.LogRecordConstants; +import cn.iocoder.yudao.module.hrm.enums.common.HrmBizTypeEnum; +import cn.iocoder.yudao.module.system.api.logger.OperateLogApi; +import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogPageReqDTO; +import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE; +import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.HRM_EMPLOYEE_TYPE; +import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.HRM_PERFORMANCE_PLAN_TYPE; +import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.HRM_PERFORMANCE_TYPE; +import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.HRM_RECRUIT_CANDIDATE_TYPE; +import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.HRM_RECRUIT_POST_TYPE; + +@Tag(name = "管理后台 - HRM 操作日志") +@RestController +@RequestMapping("/hrm/operate-log") +@Validated +public class HrmOperateLogController { + + private static final Map BIZ_TYPE_MAP = new HashMap<>(); + + static { + BIZ_TYPE_MAP.put(HrmBizTypeEnum.RECRUIT_POST.getType(), HRM_RECRUIT_POST_TYPE); + BIZ_TYPE_MAP.put(HrmBizTypeEnum.RECRUIT_CANDIDATE.getType(), HRM_RECRUIT_CANDIDATE_TYPE); + BIZ_TYPE_MAP.put(HrmBizTypeEnum.EMPLOYEE.getType(), HRM_EMPLOYEE_TYPE); + BIZ_TYPE_MAP.put(HrmBizTypeEnum.PERFORMANCE_ASSESSMENT.getType(), HRM_PERFORMANCE_TYPE); + BIZ_TYPE_MAP.put(HrmBizTypeEnum.PERFORMANCE_PLAN.getType(), HRM_PERFORMANCE_PLAN_TYPE); + } + + @Resource + private OperateLogApi operateLogApi; + + @GetMapping("/page") + @Operation(summary = "获得操作日志") + @PreAuthorize("@ss.hasAnyPermissions('hrm:recruit:post:query', 'hrm:recruit:candidate:query', " + + "'hrm:employee:query', 'hrm:performance:plan:query')") + public CommonResult> getOperateLogPage( + @Valid HrmOperateLogPageReqVO pageReqVO) { + OperateLogPageReqDTO reqDTO = new OperateLogPageReqDTO(); + reqDTO.setPageSize(PAGE_SIZE_NONE); + reqDTO.setType(BIZ_TYPE_MAP.get(pageReqVO.getBizType())); + reqDTO.setBizId(pageReqVO.getBizId()); + PageResult pageResult = operateLogApi.getOperateLogPage(reqDTO); + return success(BeanUtils.toBean(pageResult, HrmOperateLogRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/vo/HrmOperateLogPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/vo/HrmOperateLogPageReqVO.java new file mode 100644 index 0000000..ce6ad8a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/vo/HrmOperateLogPageReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.operatelog.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.common.HrmBizTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - HRM 操作日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmOperateLogPageReqVO extends PageParam { + + @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(HrmBizTypeEnum.class) + @NotNull(message = "业务类型不能为空") + private Integer bizType; + + @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "业务编号不能为空") + private Long bizId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/vo/HrmOperateLogRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/vo/HrmOperateLogRespVO.java new file mode 100644 index 0000000..1119910 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/operatelog/vo/HrmOperateLogRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.operatelog.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 操作日志 Response VO") +@Data +public class HrmOperateLogRespVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "操作用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @Schema(description = "操作用户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String userName; + + @Schema(description = "操作用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "日志类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "HRM 招聘职位") + private String type; + + @Schema(description = "日志子类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "更新招聘职位") + private String subType; + + @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long bizId; + + @Schema(description = "操作内容", requiredMode = Schema.RequiredMode.REQUIRED, + example = "更新了招聘职位【Java 开发工程师】") + private String action; + + @Schema(description = "拓展字段", example = "{}") + private String extra; + + @Schema(description = "操作时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceAssessmentController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceAssessmentController.java new file mode 100644 index 0000000..1cd6934 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceAssessmentController.java @@ -0,0 +1,202 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceArchiveEmployeePageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceArchiveEmployeeRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceArchivePageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceAssessmentBatchReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceAssessmentPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceAssessmentRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceProcessRecordRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.plan.HrmPerformancePlanRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment.HrmPerformanceAssessmentDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.plan.HrmPerformancePlanDO; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanStatusEnum; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentProcessService; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentQueryService; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentService; +import cn.iocoder.yudao.module.hrm.service.performance.plan.HrmPerformancePlanService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertLinkedSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工绩效考核") +@RestController +@RequestMapping("/hrm/performance/assessment") +@Validated +public class HrmPerformanceAssessmentController { + + @Resource + private HrmPerformanceAssessmentService performanceAssessmentService; + @Resource + private HrmPerformanceAssessmentQueryService performanceAssessmentQueryService; + @Resource + private HrmPerformancePlanService performancePlanService; + @Resource + private HrmEmployeeService employeeService; + + @PostMapping("/create-list") + @Operation(summary = "添加员工绩效考核") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult addPerformancePlanEmployees( + @Valid @RequestBody HrmPerformanceAssessmentBatchReqVO reqVO) { + performancePlanService.addPerformancePlanEmployees(reqVO); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "移除员工绩效考核") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult removePerformancePlanEmployees( + @Valid @RequestBody HrmPerformanceAssessmentBatchReqVO reqVO) { + performancePlanService.removePerformancePlanEmployees(reqVO); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得员工绩效考核分页") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult> getPerformanceAssessmentPage( + @Validated HrmPerformanceAssessmentPageReqVO reqVO) { + PageResult pageResult = + performanceAssessmentService.getPerformanceAssessmentPage(reqVO); + return success(performanceAssessmentQueryService.getPerformanceAssessmentRespVOPage(pageResult)); + } + + @GetMapping("/unassigned-employee-id-list") + @Operation(summary = "获得未加入指定绩效计划的员工编号列表") + @Parameter(name = "planId", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult> getPerformancePlanUnassignedEmployeeIdList( + @RequestParam("planId") Long planId) { + performancePlanService.validatePerformancePlanExists(planId); + // 1. 查询已经参与计划的员工 + List assessments = + performanceAssessmentService.getPerformanceAssessmentListByPlanId(planId); + Set assignedEmployeeIds = convertSet(assessments, HrmPerformanceAssessmentDO::getEmployeeId); + // 2. 筛选尚未参与计划的员工 + List employees = employeeService.getEmployeeList(new HrmEmployeeListReqVO()); + return success(convertList(employees, HrmEmployeeDO::getId, + employee -> !assignedEmployeeIds.contains(employee.getId()))); + } + + @GetMapping("/get") + @Operation(summary = "获得员工绩效考核") + @Parameter(name = "id", description = "员工绩效考核编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult getPerformanceAssessment(@RequestParam("id") Long id) { + HrmPerformanceAssessmentDO assessment = performanceAssessmentService.getPerformanceAssessment(id); + return success(performanceAssessmentQueryService.getPerformanceAssessmentProcessRespVO( + assessment, getLoginUserId())); + } + + @GetMapping("/process-record-list") + @Operation(summary = "获得绩效流程记录列表") + @Parameter(name = "id", description = "员工绩效考核编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult> getPerformanceAssessmentProcessRecordList( + @RequestParam("id") Long id) { + HrmPerformanceAssessmentDO assessment = performanceAssessmentService.getPerformanceAssessment(id); + return success(performanceAssessmentQueryService.getPerformanceAssessmentProcessRecordList(assessment)); + } + + @GetMapping("/archive-page") + @Operation(summary = "获得绩效归档分页") + @PreAuthorize("@ss.hasPermission('hrm:performance:archive:query')") + public CommonResult> getPerformanceAssessmentArchivePage( + @Validated HrmPerformanceArchivePageReqVO reqVO) { + PageResult pageResult = + performanceAssessmentService.getPerformanceAssessmentArchivePage(reqVO); + return success(performanceAssessmentQueryService.getPerformanceAssessmentRespVOPage(pageResult)); + } + + @GetMapping("/archive-employee-page") + @Operation(summary = "获得员工绩效档案分页") + @PreAuthorize("@ss.hasPermission('hrm:performance:archive:query')") + public CommonResult> getPerformanceArchiveEmployeePage( + @Validated HrmPerformanceArchiveEmployeePageReqVO reqVO) { + PageResult pageResult = + performanceAssessmentService.getPerformanceArchiveEmployeePage(reqVO); + return success(performanceAssessmentQueryService.getPerformanceArchiveEmployeeRespVOPage(pageResult)); + } + + @GetMapping("/archive-get") + @Operation(summary = "获得绩效归档详情") + @Parameter(name = "id", description = "员工绩效考核编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:archive:query')") + public CommonResult getPerformanceAssessmentArchive(@RequestParam("id") Long id) { + HrmPerformanceAssessmentDO assessment = performanceAssessmentService.getPerformanceAssessmentArchive(id); + return success(performanceAssessmentQueryService.getPerformanceAssessmentRespVO(assessment)); + } + + @GetMapping("/archive-process-record-list") + @Operation(summary = "获得绩效归档流程记录列表") + @Parameter(name = "id", description = "员工绩效考核编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:archive:query')") + public CommonResult> getPerformanceAssessmentArchiveProcessRecordList( + @RequestParam("id") Long id) { + HrmPerformanceAssessmentDO assessment = performanceAssessmentService.getPerformanceAssessmentArchive(id); + return success(performanceAssessmentQueryService.getPerformanceAssessmentProcessRecordList(assessment)); + } + + @GetMapping("/archive-plan-simple-list") + @Operation(summary = "获得绩效归档计划精简列表") + @PreAuthorize("@ss.hasPermission('hrm:performance:archive:query')") + public CommonResult> getPerformanceArchivePlanSimpleList() { + // 1. 查询存在归档考核的绩效计划 + List assessments = + performanceAssessmentService.getPerformanceAssessmentListByStatus( + HrmPerformancePlanStatusEnum.ARCHIVED.getStatus()); + Set planIds = convertLinkedSet(assessments, HrmPerformanceAssessmentDO::getPlanId); + List plans = performancePlanService.getPerformancePlanList(planIds); + // 2. 转换响应 + return success(convertList(plans, plan -> new HrmPerformancePlanRespVO() + .setId(plan.getId()).setName(plan.getName()))); + } + + @DeleteMapping("/archive-delete") + @Operation(summary = "删除绩效归档记录") + @Parameter(name = "ids", description = "绩效归档记录编号列表", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:performance:archive:delete')") + public CommonResult deletePerformanceArchiveRecords( + @RequestParam("ids") @NotEmpty(message = "归档记录不能为空") List ids) { + performanceAssessmentService.deletePerformanceArchiveRecords(ids); + return success(true); + } + + @DeleteMapping("/archive-employee-delete") + @Operation(summary = "删除员工的全部绩效档案") + @Parameter(name = "employeeIds", description = "员工编号列表", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:performance:archive:delete')") + public CommonResult deletePerformanceArchiveEmployeeRecords( + @RequestParam("employeeIds") @NotEmpty(message = "员工不能为空") List employeeIds) { + performanceAssessmentService.deletePerformanceArchiveRecordsByEmployeeIds(employeeIds); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceAssessmentTemplateController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceAssessmentTemplateController.java new file mode 100644 index 0000000..24b4a5b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceAssessmentTemplateController.java @@ -0,0 +1,148 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate.HrmPerformanceAssessmentTemplatePageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate.HrmPerformanceAssessmentTemplateRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate.HrmPerformanceAssessmentTemplateSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.config.HrmPerformanceAssessmentTemplateDO; +import cn.iocoder.yudao.module.hrm.service.performance.config.HrmPerformanceAssessmentTemplateService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +@Tag(name = "管理后台 - HRM 绩效考核模板") +@RestController +@RequestMapping("/hrm/performance/assessment-template") +@Validated +public class HrmPerformanceAssessmentTemplateController { + + @Resource + private HrmPerformanceAssessmentTemplateService assessmentTemplateService; + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建绩效考核模板") + @PreAuthorize("@ss.hasPermission('hrm:performance:assessment-template:create')") + public CommonResult createPerformanceAssessmentTemplate( + @Valid @RequestBody HrmPerformanceAssessmentTemplateSaveReqVO createReqVO) { + return success(assessmentTemplateService.createPerformanceAssessmentTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新绩效考核模板") + @PreAuthorize("@ss.hasPermission('hrm:performance:assessment-template:update')") + public CommonResult updatePerformanceAssessmentTemplate( + @Valid @RequestBody HrmPerformanceAssessmentTemplateSaveReqVO updateReqVO) { + assessmentTemplateService.updatePerformanceAssessmentTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除绩效考核模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:assessment-template:delete')") + public CommonResult deletePerformanceAssessmentTemplate(@RequestParam("id") Long id) { + assessmentTemplateService.deletePerformanceAssessmentTemplate(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除绩效考核模板") + @Parameter(name = "ids", description = "模板编号列表", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:performance:assessment-template:delete')") + public CommonResult deletePerformanceAssessmentTemplateList(@RequestParam("ids") List ids) { + assessmentTemplateService.deletePerformanceAssessmentTemplateList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得绩效考核模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:assessment-template:query')") + public CommonResult getPerformanceAssessmentTemplate( + @RequestParam("id") Long id) { + // 1. 获得考核模板 + HrmPerformanceAssessmentTemplateDO template = + assessmentTemplateService.getPerformanceAssessmentTemplate(id); + // 2. 拼接数据 + return success(buildAssessmentTemplateRespVO(template)); + } + + @GetMapping("/page") + @Operation(summary = "获得绩效考核模板分页") + @PreAuthorize("@ss.hasPermission('hrm:performance:assessment-template:query')") + public CommonResult> getPerformanceAssessmentTemplatePage( + @Validated HrmPerformanceAssessmentTemplatePageReqVO pageReqVO) { + PageResult pageResult = + assessmentTemplateService.getPerformanceAssessmentTemplatePage(pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + // 2. 拼接数据 + return success(new PageResult<>(buildAssessmentTemplateRespVOList(pageResult.getList()), + pageResult.getTotal())); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得绩效考核模板精简列表") + @PreAuthorize("@ss.hasPermission('hrm:performance:assessment-template:query')") + public CommonResult> getPerformanceAssessmentTemplateSimpleList() { + List templates = assessmentTemplateService + .getPerformanceAssessmentTemplateListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(BeanUtils.toBean(templates, HrmPerformanceAssessmentTemplateRespVO.class)); + } + + // ==================== 拼接 VO ==================== + + private HrmPerformanceAssessmentTemplateRespVO buildAssessmentTemplateRespVO( + HrmPerformanceAssessmentTemplateDO template) { + if (template == null) { + return null; + } + return CollUtil.getFirst(buildAssessmentTemplateRespVOList(singletonList(template))); + } + + private List buildAssessmentTemplateRespVOList( + List templates) { + if (CollUtil.isEmpty(templates)) { + return emptyList(); + } + // 1. 获得创建人信息 + Map userMap = adminUserApi.getUserMap( + convertSet(templates, template -> NumberUtils.parseLong(template.getCreator()))); + // 2. 拼接响应 + return BeanUtils.toBean(templates, HrmPerformanceAssessmentTemplateRespVO.class, template -> + MapUtils.findAndThen(userMap, NumberUtils.parseLong(template.getCreator()), + user -> template.setCreatorName(user.getNickname()))); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformancePlanController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformancePlanController.java new file mode 100644 index 0000000..e2398ea --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformancePlanController.java @@ -0,0 +1,258 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformancePlanResultLevelCountRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformancePlanStageCountRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.plan.HrmPerformancePlanPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.plan.HrmPerformancePlanRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.plan.HrmPerformancePlanSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment.HrmPerformanceAssessmentDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment.HrmPerformanceAssessmentStageDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.plan.HrmPerformancePlanDO; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAssessmentProcessStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanScopeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceStageTypeEnum; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentService; +import cn.iocoder.yudao.module.hrm.service.performance.plan.HrmPerformancePlanService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 绩效计划") +@RestController +@RequestMapping("/hrm/performance/plan") +@Validated +public class HrmPerformancePlanController { + + @Resource + private HrmPerformancePlanService performancePlanService; + @Resource + private HrmPerformanceAssessmentService performanceAssessmentService; + + @PostMapping("/create") + @Operation(summary = "创建绩效计划") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:create')") + public CommonResult createPerformancePlan( + @Valid @RequestBody HrmPerformancePlanSaveReqVO reqVO) { + return success(performancePlanService.createPerformancePlan(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新绩效计划") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult updatePerformancePlan(@Valid @RequestBody HrmPerformancePlanSaveReqVO reqVO) { + performancePlanService.updatePerformancePlan(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除绩效计划") + @Parameter(name = "id", description = "计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:delete')") + public CommonResult deletePerformancePlan(@RequestParam("id") Long id) { + performancePlanService.deletePerformancePlan(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得绩效计划") + @Parameter(name = "id", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult getPerformancePlan(@RequestParam("id") Long id) { + return success(buildPlanRespVO(performancePlanService.getPerformancePlan(id))); + } + + @GetMapping("/page") + @Operation(summary = "获得绩效计划分页") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult> getPerformancePlanPage( + @Validated HrmPerformancePlanPageReqVO reqVO) { + PageResult pageResult = performancePlanService.getPerformancePlanPage(reqVO); + return success(new PageResult<>(buildPlanRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @PostMapping("/start") + @Operation(summary = "启动绩效计划") + @Parameter(name = "id", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult startPerformancePlan(@RequestParam("id") Long id) { + performancePlanService.startPerformancePlan(id); + return success(true); + } + + @PostMapping("/open-scoring") + @Operation(summary = "开启绩效评分") + @Parameter(name = "id", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult openPerformancePlanScoring(@RequestParam("id") Long id) { + performancePlanService.openPerformancePlanScoring(id); + return success(true); + } + + @PostMapping("/start-interview") + @Operation(summary = "发起绩效面谈") + @Parameter(name = "id", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult startPerformancePlanInterview(@RequestParam("id") Long id) { + performancePlanService.startPerformancePlanInterview(id); + return success(true); + } + + @PostMapping("/archive") + @Operation(summary = "归档绩效计划") + @Parameter(name = "id", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult archivePerformancePlan(@RequestParam("id") Long id) { + performancePlanService.archivePerformancePlan(id); + return success(true); + } + + @PostMapping("/terminate") + @Operation(summary = "终止绩效计划") + @Parameter(name = "id", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:update')") + public CommonResult terminatePerformancePlan(@RequestParam("id") Long id) { + performancePlanService.terminatePerformancePlan(getLoginUserId(), id); + return success(true); + } + + @GetMapping("/status-count") + @Operation(summary = "获得绩效计划状态统计") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult> getPerformancePlanStatusCount( + @Validated HrmPerformancePlanPageReqVO reqVO) { + return success(performancePlanService.getPerformancePlanStatusCount(reqVO)); + } + + @GetMapping("/stage-count") + @Operation(summary = "获得绩效计划阶段统计") + @Parameter(name = "planId", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult> getPerformancePlanStageCount( + @RequestParam("planId") Long planId) { + performancePlanService.validatePerformancePlanExists(planId); + // 1.1 查询绩效计划的员工考核 + List assessments = + performanceAssessmentService.getPerformanceAssessmentListByPlanId(planId); + // 1.2 按考核阶段统计 + Map countMap = convertMap( + filterList(assessments, assessment -> assessment.getStageType() != null), + HrmPerformanceAssessmentDO::getStageType, assessment -> 1L, Long::sum, LinkedHashMap::new); + // 2. 转换响应 + return success(convertList(Arrays.asList(HrmPerformanceStageTypeEnum.ARRAYS), + stageType -> new HrmPerformancePlanStageCountRespVO() + .setStageType(stageType).setCount(countMap.get(stageType)), + stageType -> countMap.getOrDefault(stageType, 0L) > 0)); + } + + @GetMapping("/level-count") + @Operation(summary = "获得绩效结果等级统计") + @Parameter(name = "planId", description = "绩效计划编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:plan:query')") + public CommonResult> getPerformancePlanLevelCount( + @RequestParam("planId") Long planId) { + HrmPerformancePlanDO plan = performancePlanService.getPerformancePlan(planId); + if (plan == null) { + return success(Collections.emptyList()); + } + // 1.1 查询绩效计划的员工考核 + List assessments = + performanceAssessmentService.getPerformanceAssessmentListByPlanId(planId); + // 1.2 按结果模板顺序初始化全部等级,再累计实际人数 + Map levelCountMap = new LinkedHashMap<>(); + plan.getResultConfig().getLevels().forEach(level -> levelCountMap.put(level.getName(), 0L)); + filterList(assessments, assessment -> StrUtil.isNotBlank(assessment.getResultLevel())) + .forEach(assessment -> levelCountMap.merge(assessment.getResultLevel(), 1L, Long::sum)); + // 2. 转换响应 + return success(convertList(levelCountMap.entrySet(), entry -> new HrmPerformancePlanResultLevelCountRespVO() + .setLevelName(entry.getKey()).setCount(entry.getValue()))); + } + + // ==================== 拼接 VO ==================== + + private HrmPerformancePlanRespVO buildPlanRespVO(HrmPerformancePlanDO plan) { + return plan == null ? null : CollUtil.getFirst(buildPlanRespVOList(Collections.singletonList(plan))); + } + + private List buildPlanRespVOList(List plans) { + // 1. 获得员工考核和阶段信息 + List assessments = + performanceAssessmentService.getPerformanceAssessmentListByPlanIds( + convertList(plans, HrmPerformancePlanDO::getId)); + Map> assessmentMap = + convertMultiMap(assessments, HrmPerformanceAssessmentDO::getPlanId); + List stages = performanceAssessmentService.getPerformanceAssessmentStageList( + convertSet(assessments, HrmPerformanceAssessmentDO::getId)); + Map> stageMap = + convertMultiMap(stages, HrmPerformanceAssessmentStageDO::getAssessmentId); + // 2. 拼接响应 + Map planMap = convertMap(plans, HrmPerformancePlanDO::getId); + return BeanUtils.toBean(plans, HrmPerformancePlanRespVO.class, respVO -> { + HrmPerformancePlanDO plan = planMap.get(respVO.getId()); + // 排除员工范围只用于计划内部维护,不返回给前端编辑 + respVO.setScopes(filterList(respVO.getScopes(), scope -> ObjUtil.notEqual(scope.getType(), + HrmPerformancePlanScopeTypeEnum.EXCLUDED_EMPLOYEE.getType()))); + if (plan.getAssessmentConfig() != null) { + respVO.setAssessmentTemplateName(plan.getAssessmentConfig().getName()); + } + if (plan.getResultConfig() != null) { + respVO.setResultTemplateName(plan.getResultConfig().getName()); + } + // 获得当前计划的员工考核和处理阶段,用于统计执行进度并判断后续可操作节点 + List planAssessments = + assessmentMap.getOrDefault(plan.getId(), Collections.emptyList()); + List planStages = convertListByFlatMap( + planAssessments, assessment -> stageMap.getOrDefault( + assessment.getId(), Collections.emptyList()).stream()); + int finishedCount = getSumValue(planAssessments, assessment -> Objects.equals( + assessment.getProcessStatus(), + HrmPerformanceAssessmentProcessStatusEnum.FINISHED.getStatus()) ? 1 : 0, Integer::sum, 0); + Map stageCountMap = convertMap( + filterList(planAssessments, assessment -> assessment.getStageType() != null), + HrmPerformanceAssessmentDO::getStageType, assessment -> 1L, Long::sum, LinkedHashMap::new); + respVO.setEmployeeCount(planAssessments.size()).setFinishedCount(finishedCount) + .setStageCountMap(stageCountMap) + .setScoringReady(performanceAssessmentService + .isPerformanceAssessmentScoringReady(plan, planAssessments)) + .setInterviewReady(performanceAssessmentService + .isPerformanceAssessmentInterviewReady(plan, planAssessments, planStages)) + .setArchiveReady(performanceAssessmentService + .isPerformanceAssessmentArchiveReady(plan, planAssessments)); + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceResultTemplateController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceResultTemplateController.java new file mode 100644 index 0000000..0f87939 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/HrmPerformanceResultTemplateController.java @@ -0,0 +1,149 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.resulttemplate.HrmPerformanceResultTemplatePageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.resulttemplate.HrmPerformanceResultTemplateRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.resulttemplate.HrmPerformanceResultTemplateSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.config.HrmPerformanceResultTemplateDO; +import cn.iocoder.yudao.module.hrm.service.performance.config.HrmPerformanceResultTemplateService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +@Tag(name = "管理后台 - HRM 绩效结果模板") +@RestController +@RequestMapping("/hrm/performance/result-template") +@Validated +public class HrmPerformanceResultTemplateController { + + @Resource + private HrmPerformanceResultTemplateService resultTemplateService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建绩效结果模板") + @PreAuthorize("@ss.hasPermission('hrm:performance:result-template:create')") + public CommonResult createPerformanceResultTemplate( + @Valid @RequestBody HrmPerformanceResultTemplateSaveReqVO createReqVO) { + return success(resultTemplateService.createPerformanceResultTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新绩效结果模板") + @PreAuthorize("@ss.hasPermission('hrm:performance:result-template:update')") + public CommonResult updatePerformanceResultTemplate( + @Valid @RequestBody HrmPerformanceResultTemplateSaveReqVO updateReqVO) { + resultTemplateService.updatePerformanceResultTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除绩效结果模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:result-template:delete')") + public CommonResult deletePerformanceResultTemplate(@RequestParam("id") Long id) { + resultTemplateService.deletePerformanceResultTemplate(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除绩效结果模板") + @Parameter(name = "ids", description = "模板编号列表", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:performance:result-template:delete')") + public CommonResult deletePerformanceResultTemplateList(@RequestParam("ids") List ids) { + resultTemplateService.deletePerformanceResultTemplateList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得绩效结果模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:performance:result-template:query')") + public CommonResult getPerformanceResultTemplate( + @RequestParam("id") Long id) { + // 1. 获得结果模板 + HrmPerformanceResultTemplateDO template = resultTemplateService.getPerformanceResultTemplate(id); + // 2. 拼接数据 + return success(buildResultTemplateRespVO(template)); + } + + @GetMapping("/page") + @Operation(summary = "获得绩效结果模板分页") + @PreAuthorize("@ss.hasPermission('hrm:performance:result-template:query')") + public CommonResult> getPerformanceResultTemplatePage( + @Validated HrmPerformanceResultTemplatePageReqVO pageReqVO) { + PageResult pageResult = + resultTemplateService.getPerformanceResultTemplatePage(pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + // 2. 拼接数据 + return success(new PageResult<>(buildResultTemplateRespVOList(pageResult.getList()), + pageResult.getTotal())); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得绩效结果模板精简列表") + @Parameter(name = "status", description = "状态", example = "0") + @PreAuthorize("@ss.hasPermission('hrm:performance:result-template:query')") + public CommonResult> getPerformanceResultTemplateSimpleList( + @RequestParam(value = "status", required = false) Integer status) { + List templates = + resultTemplateService.getPerformanceResultTemplateList(status); + return success(BeanUtils.toBean(templates, HrmPerformanceResultTemplateRespVO.class)); + } + + // ==================== 拼接 VO ==================== + + private HrmPerformanceResultTemplateRespVO buildResultTemplateRespVO( + HrmPerformanceResultTemplateDO template) { + if (template == null) { + return null; + } + return CollUtil.getFirst(buildResultTemplateRespVOList(singletonList(template))); + } + + private List buildResultTemplateRespVOList( + List templates) { + if (CollUtil.isEmpty(templates)) { + return emptyList(); + } + // 1. 获得创建人信息 + Map userMap = adminUserApi.getUserMap( + convertSet(templates, template -> NumberUtils.parseLong(template.getCreator()))); + // 2. 拼接响应 + return BeanUtils.toBean(templates, HrmPerformanceResultTemplateRespVO.class, template -> + MapUtils.findAndThen(userMap, NumberUtils.parseLong(template.getCreator()), + user -> template.setCreatorName(user.getNickname()))); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchiveEmployeePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchiveEmployeePageReqVO.java new file mode 100644 index 0000000..517d787 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchiveEmployeePageReqVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 员工绩效档案分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPerformanceArchiveEmployeePageReqVO extends PageParam { + + @Schema(description = "员工姓名或工号,模糊匹配", example = "张三") + private String search; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchiveEmployeeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchiveEmployeeRespVO.java new file mode 100644 index 0000000..d18e4e1 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchiveEmployeeRespVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工绩效档案 Response VO") +@Data +public class HrmPerformanceArchiveEmployeeRespVO { + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "员工姓名", example = "张三") + private String employeeName; + + @Schema(description = "工号", example = "HRM001") + private String jobNumber; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门名称", example = "研发部") + private String deptName; + + @Schema(description = "职位名称", example = "Java 工程师") + private String postName; + + @Schema(description = "手机号码", example = "15601691300") + private String mobile; + + @Schema(description = "员工状态", example = "1") + private Integer employeeStatus; + + @Schema(description = "聘用形式", example = "1") + private Integer employeeType; + + @Schema(description = "最近考核编号", example = "2048") + private Long latestAssessmentId; + + @Schema(description = "最近考核计划", example = "2026 年度绩效考核") + private String latestPlanName; + + @Schema(description = "最近绩效评分", example = "95.50") + private BigDecimal latestScore; + + @Schema(description = "最近绩效等级", example = "A") + private String latestResultLevel; + + @Schema(description = "考核次数", example = "3") + private Long assessmentCount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchivePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchivePageReqVO.java new file mode 100644 index 0000000..8d33b2a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceArchivePageReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 绩效归档分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPerformanceArchivePageReqVO extends PageParam { + + @Schema(description = "绩效计划编号", example = "1024") + private Long planId; + + @Schema(description = "员工编号", example = "2048") + private Long employeeId; + + @Schema(description = "员工编号列表", example = "[2048, 2049]") + private List employeeIds; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentBatchReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentBatchReqVO.java new file mode 100644 index 0000000..8a8fc67 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentBatchReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 绩效计划增删员工 Request VO") +@Data +public class HrmPerformanceAssessmentBatchReqVO { + + @Schema(description = "绩效计划编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "绩效计划不能为空") + private Long planId; + + @Schema(description = "员工编号列表", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[1024, 2048]") + @NotEmpty(message = "员工不能为空") + private List employeeIds; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentDimensionRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentDimensionRespVO.java new file mode 100644 index 0000000..94dfba8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentDimensionRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工绩效维度 Response VO") +@Data +public class HrmPerformanceAssessmentDimensionRespVO { + + @Schema(description = "维度编号") + private Long id; + + @Schema(description = "员工绩效考核编号") + private Long assessmentId; + + @Schema(description = "维度名称") + private String name; + + @Schema(description = "指标类型") + private Integer quotaType; + + @Schema(description = "维度权重") + private BigDecimal weight; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否允许编辑") + private Boolean allowEdit; + + @Schema(description = "排序") + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentPageReqVO.java new file mode 100644 index 0000000..bb62fc3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentPageReqVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAppealStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceStageTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 绩效计划员工分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPerformanceAssessmentPageReqVO extends PageParam { + + @Schema(description = "绩效计划编号", example = "1024") + private Long planId; + + @Schema(description = "员工编号", example = "2048") + private Long employeeId; + + @Schema(description = "员工编号列表", example = "[2048, 2049]") + private List employeeIds; + + @Schema(description = "员工姓名或工号", example = "张三") + private String search; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "聘用形式", example = "1") + private Integer employeeType; + + @Schema(description = "员工状态", example = "1") + private Integer employeeStatus; + + @Schema(description = "考核状态", example = "1") + @InEnum(value = HrmPerformancePlanStatusEnum.class, message = "考核状态必须是 {value}") + private Integer status; + + @Schema(description = "阶段状态", example = "2") + @InEnum(value = HrmPerformanceStageTypeEnum.class, message = "阶段状态必须是 {value}") + private Integer stageType; + + @Schema(description = "申诉状态", example = "1") + @InEnum(value = HrmPerformanceAppealStatusEnum.class, message = "申诉状态必须是 {value}") + private Integer appealStatus; + + @Schema(description = "结果等级", example = "A") + private String resultLevel; + + @Schema(description = "结果等级是否为空", example = "false") + private Boolean resultLevelEmpty; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentQuotaRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentQuotaRespVO.java new file mode 100644 index 0000000..51abaf6 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentQuotaRespVO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工绩效指标 Response VO") +@Data +public class HrmPerformanceAssessmentQuotaRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "员工绩效考核编号") + private Long assessmentId; + + @Schema(description = "员工绩效维度编号") + private Long dimensionId; + + @Schema(description = "是否允许编辑") + private Boolean allowEdit; + + @Schema(description = "预设值") + private Boolean preset; + + @Schema(description = "维度名称") + private String dimensionName; + + @Schema(description = "指标名称") + private String name; + + @Schema(description = "指标说明") + private String description; + + @Schema(description = "标准值") + private String standard; + + @Schema(description = "维度权重") + private BigDecimal dimensionWeight; + + @Schema(description = "指标权重") + private BigDecimal weight; + + @Schema(description = "分数类型") + private Integer scoreType; + + @Schema(description = "目标值") + private String targetValue; + + @Schema(description = "实际值") + private String actualValue; + + @Schema(description = "自评分数") + private BigDecimal selfScore; + + @Schema(description = "评分人得分") + private BigDecimal reviewerScore; + + @Schema(description = "最终得分") + private BigDecimal finalScore; + + @Schema(description = "说明") + private String comment; + + @Schema(description = "排序") + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentQuotaScoreRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentQuotaScoreRespVO.java new file mode 100644 index 0000000..e1e66cb --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentQuotaScoreRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工绩效指标评分明细 Response VO") +@Data +public class HrmPerformanceAssessmentQuotaScoreRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "员工评分阶段编号") + private Long assessmentStageId; + + @Schema(description = "员工绩效指标编号") + private Long assessmentQuotaId; + + @Schema(description = "得分") + private BigDecimal score; + + @Schema(description = "说明") + private String comment; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentRespVO.java new file mode 100644 index 0000000..1fc3f02 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentRespVO.java @@ -0,0 +1,170 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 绩效员工 Response VO") +@Data +public class HrmPerformanceAssessmentRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "绩效计划编号") + private Long planId; + + @Schema(description = "绩效计划名称") + private String name; + + @Schema(description = "考核周期类型") + private Integer cycleType; + + @Schema(description = "考核周期") + private String cycle; + + @Schema(description = "开始日期") + private LocalDateTime startTime; + + @Schema(description = "结束日期") + private LocalDateTime endTime; + + @Schema(description = "单项评分上限") + private BigDecimal upperLimitScore; + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "员工姓名") + private String employeeName; + + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "手机号") + private String mobile; + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "职位名称") + private String postName; + + @Schema(description = "聘用形式") + private Integer employeeType; + + @Schema(description = "员工状态") + private Integer employeeStatus; + + @Schema(description = "当前处理人") + private String currentHandlerName; + + @Schema(description = "考核状态") + private Integer status; + + @Schema(description = "激活状态") + private Integer processStatus; + + @Schema(description = "阶段状态") + private Integer stageType; + + @Schema(description = "阶段排序") + private Integer stageSort; + + @Schema(description = "得分") + private BigDecimal score; + + @Schema(description = "结果等级") + private String resultLevel; + + @Schema(description = "绩效系数") + private BigDecimal coefficient; + + @Schema(description = "目标确认员工姓名") + private String targetConfirmationEmployeeName; + + @Schema(description = "目标确认结果") + private Integer targetConfirmationResult; + + @Schema(description = "目标确认意见") + private String targetConfirmationComment; + + @Schema(description = "目标确认时间") + private LocalDateTime targetConfirmationTime; + + @Schema(description = "是否可确认目标") + private Boolean canConfirmTarget; + + @Schema(description = "自评说明") + private String selfComment; + + @Schema(description = "评分人说明") + private String reviewerComment; + + @Schema(description = "结果说明") + private String resultComment; + + @Schema(description = "结果确认时间") + private LocalDateTime resultConfirmationTime; + + @Schema(description = "结果审核状态") + private Integer resultAuditStatus; + + @Schema(description = "结果审核时间") + private LocalDateTime resultAuditTime; + + @Schema(description = "结果审核意见") + private String resultAuditReason; + + @Schema(description = "申诉原因") + private String appealReason; + + @Schema(description = "申诉附件地址列表") + private List appealFileUrls; + + @Schema(description = "申诉退回评分阶段编号列表") + private List appealReviewStageIds; + + @Schema(description = "申诉提交时间") + private LocalDateTime appealSubmitTime; + + @Schema(description = "申诉状态") + private Integer appealStatus; + + @Schema(description = "申诉处理时间") + private LocalDateTime appealTime; + + @Schema(description = "申诉处理意见") + private String appealComment; + + @Schema(description = "归档时间") + private LocalDateTime archiveTime; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "指标维度列表") + private List dimensions; + + @Schema(description = "指标列表") + private List quotas; + + @Schema(description = "评分阶段列表") + private List reviewStages; + + @Schema(description = "当前评分阶段") + private HrmPerformanceAssessmentStageRespVO currentReviewStage; + + @Schema(description = "运行阶段列表") + private List stages; + + @Schema(description = "当前待处理阶段") + private HrmPerformanceAssessmentStageRespVO currentStage; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentStageRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentStageRespVO.java new file mode 100644 index 0000000..b98bbf1 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceAssessmentStageRespVO.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工绩效运行阶段 Response VO") +@Data +public class HrmPerformanceAssessmentStageRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "员工绩效考核编号") + private Long assessmentId; + + @Schema(description = "阶段类型") + private Integer type; + + @Schema(description = "处理员工编号") + private Long handlerEmployeeId; + + @Schema(description = "处理人姓名") + private String handlerName; + + @Schema(description = "阶段名称") + private String name; + + @Schema(description = "评分人类型") + private Integer raterType; + + @Schema(description = "权重") + private BigDecimal weight; + + @Schema(description = "评分类型") + private Integer scoringType; + + @Schema(description = "可见内容") + private Integer visibleContent; + + @Schema(description = "是否必填") + private Boolean requiredSetting; + + @Schema(description = "驳回权限") + private Boolean rejectAuthority; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "绩效员工评审阶段状态") + private Integer status; + + @Schema(description = "得分") + private BigDecimal score; + + @Schema(description = "结果等级") + private String resultLevel; + + @Schema(description = "说明") + private String comment; + + @Schema(description = "驳回原因") + private String rejectReason; + + @Schema(description = "提交时间") + private LocalDateTime submitTime; + + @Schema(description = "截止时间") + private LocalDateTime deadlineTime; + + @Schema(description = "是否可处理") + private Boolean canHandle; + + @Schema(description = "是否可评分") + private Boolean canScore; + + @Schema(description = "指标评分列表") + private List quotaScoreList; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformancePlanResultLevelCountRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformancePlanResultLevelCountRespVO.java new file mode 100644 index 0000000..409e03c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformancePlanResultLevelCountRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 绩效计划结果等级统计 Response VO") +@Data +public class HrmPerformancePlanResultLevelCountRespVO { + + @Schema(description = "等级名称") + private String levelName; + + @Schema(description = "数量") + private Long count; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformancePlanStageCountRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformancePlanStageCountRespVO.java new file mode 100644 index 0000000..82602e9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformancePlanStageCountRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 绩效计划阶段统计 Response VO") +@Data +public class HrmPerformancePlanStageCountRespVO { + + @Schema(description = "阶段状态") + private Integer stageType; + + @Schema(description = "数量") + private Long count; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceProcessRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceProcessRecordRespVO.java new file mode 100644 index 0000000..6a43e38 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessment/HrmPerformanceProcessRecordRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工绩效考核流程记录 Response VO") +@Data +public class HrmPerformanceProcessRecordRespVO { + + @Schema(description = "标题") + private String title; + + @Schema(description = "内容") + private String content; + + @Schema(description = "来源") + private String source; + + @Schema(description = "绩效流程记录状态") + private Integer status; + + @Schema(description = "操作人姓名") + private String operatorName; + + @Schema(description = "操作时间") + private LocalDateTime operateTime; + + @Schema(description = "附件地址列表") + private List fileUrls; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplatePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplatePageReqVO.java new file mode 100644 index 0000000..288b3b0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplatePageReqVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 绩效考核模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPerformanceAssessmentTemplatePageReqVO extends PageParam { + + @Schema(description = "模板名称,模糊匹配", example = "季度") + private String name; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplateRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplateRespVO.java new file mode 100644 index 0000000..5930ed0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplateRespVO.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 绩效考核模板 Response VO") +@Data +public class HrmPerformanceAssessmentTemplateRespVO { + + @Schema(description = "模板编号", example = "1024") + private Long id; + + @Schema(description = "模板名称", example = "季度绩效模板") + private String name; + + @Schema(description = "模板说明", example = "适用于季度绩效考核") + private String illustrate; + + @Schema(description = "计分方式", example = "1") + private Integer scoreCalculation; + + @Schema(description = "分数上限类型", example = "1") + private Integer upperLimitType; + + @Schema(description = "分数上限", example = "100") + private BigDecimal upperLimitScore; + + @Schema(description = "维度数量", example = "3") + private Integer dimensionCount; + + @Schema(description = "指标数量", example = "12") + private Integer quotaCount; + + @Schema(description = "状态", example = "0") + private Integer status; + + @Schema(description = "考核维度列表") + private List dimensions; + + @Schema(description = "创建人") + private String creator; + + @Schema(description = "创建人名称") + private String creatorName; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "管理后台 - HRM 绩效考核模板维度") + @Data + public static class Dimension { + + @Schema(description = "维度名称", example = "工作业绩") + private String name; + + @Schema(description = "指标类型", example = "1") + private Integer quotaType; + + @Schema(description = "维度权重", example = "60") + private BigDecimal weight; + + @Schema(description = "备注", example = "核心业绩指标") + private String remark; + + @Schema(description = "是否允许员工编辑", example = "true") + private Boolean allowEdit; + + @Schema(description = "考核指标列表") + private List quotas; + } + + @Schema(description = "管理后台 - HRM 绩效考核模板指标") + @Data + public static class Quota { + + @Schema(description = "指标名称", example = "销售目标达成率") + private String name; + + @Schema(description = "指标说明", example = "按季度销售目标计算") + private String illustrate; + + @Schema(description = "评分标准", example = "完成率达到 100% 得满分") + private String standard; + + @Schema(description = "指标权重", example = "50") + private BigDecimal weight; + + @Schema(description = "评分类型", example = "1") + private Integer scoreType; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplateSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplateSaveReqVO.java new file mode 100644 index 0000000..4a84b91 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/assessmenttemplate/HrmPerformanceAssessmentTemplateSaveReqVO.java @@ -0,0 +1,234 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceQuotaScoreTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceQuotaTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceScoreCalculationEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceUpperLimitTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.PERCENT_100; + +@Schema(description = "管理后台 - HRM 绩效考核模板保存 Request VO") +@Data +public class HrmPerformanceAssessmentTemplateSaveReqVO { + + @Schema(description = "模板编号", example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "季度绩效模板") + @NotBlank(message = "模板名称不能为空") + @Size(max = 50, message = "模板名称不能超过 50 个字符") + private String name; + + @Schema(description = "模板说明", example = "适用于季度绩效考核") + @Size(max = 200, message = "模板说明不能超过 200 个字符") + private String illustrate; + + @Schema(description = "计分方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "计分方式不能为空") + @InEnum(value = HrmPerformanceScoreCalculationEnum.class, message = "计分方式必须是 {value}") + private Integer scoreCalculation; + + @Schema(description = "分数上限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分数上限类型不能为空") + @InEnum(value = HrmPerformanceUpperLimitTypeEnum.class, message = "分数上限类型必须是 {value}") + private Integer upperLimitType; + + @Schema(description = "分数上限", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "分数上限不能为空") + @DecimalMin(value = "0", message = "分数上限不能小于 0") + @DecimalMax(value = "100", message = "分数上限不能大于 100") + private BigDecimal upperLimitScore; + + @Schema(description = "考核维度列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "至少需要一个考核维度") + private List<@NotNull(message = "考核维度不能为空") Dimension> dimensions; + + @AssertTrue(message = "考核维度名称不能重复") + @JsonIgnore + public boolean isDimensionNamesUnique() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + Set names = new HashSet<>(); + for (Dimension dimension : dimensions) { + if (dimension != null && StrUtil.isNotBlank(dimension.getName()) + && !names.add(dimension.getName().trim())) { + return false; + } + } + return true; + } + + @AssertTrue(message = "考核指标名称不能重复") + @JsonIgnore + public boolean isQuotaNamesUnique() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + Set names = new HashSet<>(); + for (Dimension dimension : dimensions) { + if (dimension == null || CollUtil.isEmpty(dimension.getQuotas())) { + continue; + } + for (Quota quota : dimension.getQuotas()) { + if (quota != null && StrUtil.isNotBlank(quota.getName()) + && !names.add(quota.getName().trim())) { + return false; + } + } + } + return true; + } + + @AssertTrue(message = "维度权重总和必须等于 100%") + @JsonIgnore + public boolean isDimensionWeightTotalValid() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + BigDecimal totalWeight = BigDecimal.ZERO; + for (Dimension dimension : dimensions) { + if (dimension == null || dimension.getWeight() == null) { + return true; + } + totalWeight = totalWeight.add(dimension.getWeight()); + } + return totalWeight.compareTo(PERCENT_100) == 0; + } + + @AssertTrue(message = "不可编辑维度的指标权重总和必须等于 100%") + @JsonIgnore + public boolean isFixedQuotaWeightTotalValid() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + for (Dimension dimension : dimensions) { + if (dimension == null || ObjUtil.notEqual(Boolean.FALSE, dimension.getAllowEdit()) + || CollUtil.isEmpty(dimension.getQuotas())) { + continue; + } + BigDecimal totalWeight = BigDecimal.ZERO; + for (Quota quota : dimension.getQuotas()) { + if (quota == null || quota.getWeight() == null) { + return true; + } + totalWeight = totalWeight.add(quota.getWeight()); + } + if (totalWeight.compareTo(PERCENT_100) != 0) { + return false; + } + } + return true; + } + + @AssertTrue(message = "可编辑维度的指标权重总和不能大于 100%") + @JsonIgnore + public boolean isEditableQuotaWeightTotalValid() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + for (Dimension dimension : dimensions) { + if (dimension == null || ObjUtil.notEqual(Boolean.TRUE, dimension.getAllowEdit()) + || CollUtil.isEmpty(dimension.getQuotas())) { + continue; + } + BigDecimal totalWeight = BigDecimal.ZERO; + for (Quota quota : dimension.getQuotas()) { + if (quota == null || quota.getWeight() == null) { + return true; + } + totalWeight = totalWeight.add(quota.getWeight()); + } + if (totalWeight.compareTo(PERCENT_100) > 0) { + return false; + } + } + return true; + } + + @Schema(description = "管理后台 - HRM 绩效考核模板维度") + @Data + public static class Dimension { + + @Schema(description = "维度名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作业绩") + @NotBlank(message = "维度名称不能为空") + @Size(max = 50, message = "维度名称不能超过 50 个字符") + private String name; + + @Schema(description = "指标类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "指标类型不能为空") + @InEnum(value = HrmPerformanceQuotaTypeEnum.class, message = "指标类型必须是 {value}") + private Integer quotaType; + + @Schema(description = "维度权重", requiredMode = Schema.RequiredMode.REQUIRED, example = "60") + @NotNull(message = "维度权重不能为空") + @DecimalMin(value = "0", message = "维度权重不能小于 0") + @DecimalMax(value = "100", message = "维度权重不能大于 100") + private BigDecimal weight; + + @Schema(description = "备注", example = "核心业绩指标") + @Size(max = 200, message = "维度备注不能超过 200 个字符") + private String remark; + + @Schema(description = "是否允许员工编辑", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否允许员工编辑不能为空") + private Boolean allowEdit; + + @Schema(description = "考核指标列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "每个考核维度至少需要一个指标") + private List<@NotNull(message = "考核指标不能为空") Quota> quotas; + } + + @Schema(description = "管理后台 - HRM 绩效考核模板指标") + @Data + public static class Quota { + + @Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "销售目标达成率") + @NotBlank(message = "指标名称不能为空") + @Size(max = 50, message = "指标名称不能超过 50 个字符") + private String name; + + @Schema(description = "指标说明", example = "按季度销售目标计算") + @Size(max = 200, message = "指标说明不能超过 200 个字符") + private String illustrate; + + @Schema(description = "评分标准", requiredMode = Schema.RequiredMode.REQUIRED, example = "完成率达到 100% 得满分") + @NotBlank(message = "评分标准不能为空") + @Size(max = 200, message = "评分标准不能超过 200 个字符") + private String standard; + + @Schema(description = "指标权重", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + @NotNull(message = "指标权重不能为空") + @DecimalMin(value = "0", message = "指标权重不能小于 0") + @DecimalMax(value = "100", message = "指标权重不能大于 100") + private BigDecimal weight; + + @Schema(description = "评分类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "评分类型不能为空") + @InEnum(value = HrmPerformanceQuotaScoreTypeEnum.class, message = "评分类型必须是 {value}") + private Integer scoreType; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanPageReqVO.java new file mode 100644 index 0000000..917dc1c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanPageReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.plan; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceStageTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 绩效计划分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPerformancePlanPageReqVO extends PageParam { + + @Schema(description = "绩效计划名称", example = "2026 年第三季度绩效计划") + private String name; + + @Schema(description = "绩效计划状态", example = "1") + @InEnum(value = HrmPerformancePlanStatusEnum.class, message = "计划状态必须是 {value}") + private Integer status; + + @Schema(description = "阶段状态", example = "2") + @InEnum(value = HrmPerformanceStageTypeEnum.class, message = "阶段状态必须是 {value}") + private Integer stageType; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanRespVO.java new file mode 100644 index 0000000..eed7f5d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanRespVO.java @@ -0,0 +1,280 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.plan; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - HRM 绩效计划 Response VO") +@Data +public class HrmPerformancePlanRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "绩效计划名称") + private String name; + + @Schema(description = "考核周期类型") + private Integer cycleType; + + @Schema(description = "考核周期") + private String cycle; + + @Schema(description = "季度") + private Integer quarter; + + @Schema(description = "开始日期") + private LocalDateTime startTime; + + @Schema(description = "结束日期") + private LocalDateTime endTime; + + @Schema(description = "考核说明") + private String description; + + @Schema(description = "考评范围列表") + private List scopes; + + @Schema(description = "考核模板编号") + private Long assessmentTemplateId; + + @Schema(description = "考核配置快照") + private AssessmentConfig assessmentConfig; + + @Schema(description = "结果模板编号") + private Long resultTemplateId; + + @Schema(description = "结果配置快照") + private ResultConfig resultConfig; + + @Schema(description = "指标设置类型") + private Integer quotaSettingType; + + @Schema(description = "是否需要目标确认") + private Boolean targetConfirmation; + + @Schema(description = "目标确认节点") + private HandlerStage targetConfirmationStage; + + @Schema(description = "评分阶段列表") + private List reviewStages; + + @Schema(description = "是否需要结果审核") + private Boolean resultAudit; + + @Schema(description = "结果审核节点列表") + private List resultAuditStages; + + @Schema(description = "是否需要结果确认") + private Boolean resultConfirmation; + + @Schema(description = "申诉处理节点列表") + private List appealStages; + + @Schema(description = "申诉超期天数") + private Integer appealTimeoutDays; + + @Schema(description = "申诉超期处理方式") + private Integer appealTimeoutAction; + + @Schema(description = "是否同步薪资") + private Boolean syncToSalary; + + @Schema(description = "计薪月份") + private String paidForMonth; + + @Schema(description = "各阶段员工数量") + private Map stageCountMap; + + @Schema(description = "阶段状态") + private Integer stageType; + + @Schema(description = "绩效计划状态") + private Integer status; + + @Schema(description = "当前操作阶段") + private Integer operationType; + + @Schema(description = "终止时间") + private LocalDateTime terminateTime; + + @Schema(description = "考核模板名称") + private String assessmentTemplateName; + + @Schema(description = "结果模板名称") + private String resultTemplateName; + + @Schema(description = "员工数量") + private Integer employeeCount; + + @Schema(description = "已完成数量") + private Integer finishedCount; + + @Schema(description = "是否可开始评分") + private Boolean scoringReady; + + @Schema(description = "是否可开始面谈") + private Boolean interviewReady; + + @Schema(description = "是否可归档") + private Boolean archiveReady; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "管理后台 - HRM 绩效计划考评范围 Response VO") + @Data + public static class Scope { + + @Schema(description = "范围类型", example = "1") + private Integer type; + + @Schema(description = "员工编号列表", example = "[1024, 1025]") + private List employeeIds; + + @Schema(description = "部门编号列表", example = "[100, 101]") + private List deptIds; + + @Schema(description = "聘用形式", example = "1") + private Integer employeeType; + + @Schema(description = "员工状态列表", example = "[1, 2]") + private List employeeStatuses; + } + + @Schema(description = "管理后台 - HRM 绩效计划处理节点 Response VO") + @Data + public static class HandlerStage { + + @Schema(description = "处理人类型", example = "1") + private Integer type; + + @Schema(description = "上级或部门层级", example = "1") + private Integer level; + + @Schema(description = "指定处理员工编号", example = "1024") + private Long employeeId; + } + + @Schema(description = "管理后台 - HRM 绩效计划评分阶段 Response VO") + @Data + public static class ReviewStage { + + @Schema(description = "评分阶段名称", example = "直属上级评分") + private String name; + + @Schema(description = "评分人") + private HandlerStage rater; + + @Schema(description = "评分权重", example = "60") + private BigDecimal weight; + + @Schema(description = "评分方式", example = "1") + private Integer scoringType; + + @Schema(description = "评分内容可见范围", example = "1") + private Integer visibleContent; + + @Schema(description = "评语是否必填", example = "true") + private Boolean requiredSetting; + + @Schema(description = "是否允许驳回", example = "true") + private Boolean rejectAuthority; + } + + @Schema(description = "管理后台 - HRM 绩效考核配置快照 Response VO") + @Data + public static class AssessmentConfig { + + @Schema(description = "模板名称", example = "季度绩效模板") + private String name; + + @Schema(description = "计分方式", example = "1") + private Integer scoreCalculation; + + @Schema(description = "分数上限类型", example = "1") + private Integer upperLimitType; + + @Schema(description = "分数上限", example = "100") + private BigDecimal upperLimitScore; + + @Schema(description = "考核维度配置") + private List dimensions; + + @Schema(description = "管理后台 - HRM 绩效考核维度 Response VO") + @Data + public static class Dimension { + + @Schema(description = "维度名称", example = "工作业绩") + private String name; + + @Schema(description = "指标类型", example = "1") + private Integer quotaType; + + @Schema(description = "维度权重", example = "60") + private BigDecimal weight; + + @Schema(description = "备注", example = "核心业绩指标") + private String remark; + + @Schema(description = "是否允许员工编辑", example = "true") + private Boolean allowEdit; + + @Schema(description = "考核指标列表") + private List quotas; + } + + @Schema(description = "管理后台 - HRM 绩效考核指标 Response VO") + @Data + public static class Quota { + + @Schema(description = "指标名称", example = "销售目标达成率") + private String name; + + @Schema(description = "指标说明", example = "按季度销售目标计算") + private String illustrate; + + @Schema(description = "评分标准", example = "完成率达到 100% 得满分") + private String standard; + + @Schema(description = "指标权重", example = "50") + private BigDecimal weight; + + @Schema(description = "评分类型", example = "1") + private Integer scoreType; + } + } + + @Schema(description = "管理后台 - HRM 绩效结果配置快照 Response VO") + @Data + public static class ResultConfig { + + @Schema(description = "模板名称", example = "绩效结果模板") + private String name; + + @Schema(description = "结果等级配置") + private List levels; + + @Schema(description = "管理后台 - HRM 绩效结果等级 Response VO") + @Data + public static class Level { + + @Schema(description = "等级名称", example = "优秀") + private String name; + + @Schema(description = "最低分数", example = "90") + private BigDecimal minScore; + + @Schema(description = "最高分数", example = "100") + private BigDecimal maxScore; + + @Schema(description = "绩效系数", example = "1.2") + private BigDecimal coefficient; + } + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanSaveReqVO.java new file mode 100644 index 0000000..a3a0b5a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/plan/HrmPerformancePlanSaveReqVO.java @@ -0,0 +1,665 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.plan; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate.HrmPerformanceAssessmentTemplateSaveReqVO.Dimension; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessmenttemplate.HrmPerformanceAssessmentTemplateSaveReqVO.Quota; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.resulttemplate.HrmPerformanceResultTemplateSaveReqVO.Level; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAppealTimeoutActionEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceCycleTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanScopeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceQuarterEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceQuotaSettingTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceRaterTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceReviewScoringTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceReviewVisibleContentEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceScoreCalculationEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceUpperLimitTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.PERCENT_100; + +@Schema(description = "管理后台 - HRM 绩效计划保存 Request VO") +@Data +public class HrmPerformancePlanSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "绩效计划名称", requiredMode = Schema.RequiredMode.REQUIRED, + example = "2026 年第三季度绩效计划") + @NotBlank(message = "计划名称不能为空") + @Size(max = 50, message = "计划名称不能超过 50 个字符") + private String name; + + @Schema(description = "考核周期类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "考核周期类型不能为空") + @InEnum(value = HrmPerformanceCycleTypeEnum.class, message = "考核周期类型必须是 {value}") + private Integer cycleType; + + @Schema(description = "考核周期", example = "2026-08") + @Size(max = 255, message = "考核周期不能超过 255 个字符") + private String cycle; + + @Schema(description = "季度", example = "3") + @InEnum(value = HrmPerformanceQuarterEnum.class, message = "季度必须是 {value}") + private Integer quarter; + + @Schema(description = "开始日期") + private LocalDateTime startTime; + + @Schema(description = "结束日期") + private LocalDateTime endTime; + + @Schema(description = "考核说明", example = "2026 年第三季度绩效考核") + @Size(max = 200, message = "考核说明不能超过 200 个字符") + private String description; + + @Schema(description = "考评范围列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "考评范围不能为空") + @Size(max = 3, message = "考评范围不能超过 3 个") + private List<@NotNull(message = "考评范围不能为空") Scope> scopes; + + @Schema(description = "考核模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "考核模板不能为空") + private Long assessmentTemplateId; + + @Schema(description = "考核配置快照", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "考核配置不能为空") + private AssessmentConfig assessmentConfig; + + @Schema(description = "指标设置类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "指标设置类型不能为空") + @InEnum(value = HrmPerformanceQuotaSettingTypeEnum.class, message = "指标设置类型必须是 {value}") + private Integer quotaSettingType; + + @Schema(description = "是否需要目标确认", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否需要目标确认不能为空") + private Boolean targetConfirmation; + + @Schema(description = "目标确认节点") + @Valid + private HandlerStage targetConfirmationStage; + + @Schema(description = "评分阶段列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "评分阶段不能为空") + private List<@NotNull(message = "评分阶段不能为空") ReviewStage> reviewStages; + + @Schema(description = "是否需要结果审核", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否需要结果审核不能为空") + private Boolean resultAudit; + + @Schema(description = "结果审核节点列表") + @Valid + private List<@NotNull(message = "结果审核节点不能为空") HandlerStage> resultAuditStages; + + @Schema(description = "是否需要结果确认", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否需要结果确认不能为空") + private Boolean resultConfirmation; + + @Schema(description = "申诉处理节点列表") + @Valid + private List<@NotNull(message = "申诉处理节点不能为空") HandlerStage> appealStages; + + @Schema(description = "申诉超期天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @Min(value = 1, message = "申诉超期天数不能小于 1 天") + @Max(value = 100, message = "申诉超期天数不能超过 100 天") + @NotNull(message = "申诉超期天数不能为空") + private Integer appealTimeoutDays; + + @Schema(description = "申诉超期处理方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(HrmPerformanceAppealTimeoutActionEnum.class) + @NotNull(message = "申诉超期处理方式不能为空") + private Integer appealTimeoutAction; + + @Schema(description = "结果模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "结果模板不能为空") + private Long resultTemplateId; + + @Schema(description = "结果配置快照", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "结果配置不能为空") + private ResultConfig resultConfig; + + @Schema(description = "是否同步薪资", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否同步薪资不能为空") + private Boolean syncToSalary; + + @Schema(description = "计薪月份", example = "2026-09") + @Size(max = 20, message = "计薪月份不能超过 20 个字符") + private String paidForMonth; + + @Schema(description = "管理后台 - HRM 绩效计划考评范围") + @Data + public static class Scope { + + @Schema(description = "范围类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "考评范围类型不能为空") + @InEnum(value = HrmPerformancePlanScopeTypeEnum.class, message = "考评范围类型必须是 {value}") + private Integer type; + + @Schema(description = "员工编号列表", example = "[1024, 1025]") + private List<@NotNull(message = "员工编号不能为空") Long> employeeIds; + + @Schema(description = "部门编号列表", example = "[100, 101]") + private List<@NotNull(message = "部门编号不能为空") Long> deptIds; + + @Schema(description = "聘用形式", example = "1") + @InEnum(value = HrmEmployeeTypeEnum.class, message = "聘用形式必须是 {value}") + private Integer employeeType; + + @Schema(description = "员工状态列表", example = "[1, 2]") + @InEnum(value = HrmEmployeeStatusEnum.class, message = "员工状态必须是 {value}") + private List employeeStatuses; + } + + @Schema(description = "管理后台 - HRM 绩效计划处理节点") + @Data + public static class HandlerStage { + + @Schema(description = "处理人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "处理人类型不能为空") + @InEnum(value = HrmPerformanceRaterTypeEnum.class, message = "处理人类型必须是 {value}") + private Integer type; + + @Schema(description = "上级或部门层级", example = "1") + @Min(value = 1, message = "处理人层级不能小于 1") + @Max(value = 10, message = "处理人层级不能大于 10") + private Integer level; + + @Schema(description = "指定处理员工编号", example = "1024") + private Long employeeId; + } + + @Schema(description = "管理后台 - HRM 绩效计划评分阶段") + @Data + public static class ReviewStage { + + @Schema(description = "评分阶段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "直属上级评分") + @NotBlank(message = "评分阶段名称不能为空") + @Size(max = 50, message = "评分阶段名称不能超过 50 个字符") + private String name; + + @Schema(description = "评分人", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "评分人不能为空") + private HandlerStage rater; + + @Schema(description = "评分权重", requiredMode = Schema.RequiredMode.REQUIRED, example = "60") + @NotNull(message = "评分权重不能为空") + @DecimalMin(value = "0.01", message = "评分权重必须大于 0") + @DecimalMax(value = "100", message = "评分权重不能大于 100") + private BigDecimal weight; + + @Schema(description = "评分方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "评分方式不能为空") + @InEnum(value = HrmPerformanceReviewScoringTypeEnum.class, message = "评分方式必须是 {value}") + private Integer scoringType; + + @Schema(description = "评分内容可见范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "评分内容可见范围不能为空") + @InEnum(value = HrmPerformanceReviewVisibleContentEnum.class, message = "评分内容可见范围必须是 {value}") + private Integer visibleContent; + + @Schema(description = "评语是否必填", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "评语是否必填不能为空") + private Boolean requiredSetting; + + @Schema(description = "是否允许驳回", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否允许驳回不能为空") + private Boolean rejectAuthority; + } + + @Schema(description = "管理后台 - HRM 绩效考核配置快照") + @Data + public static class AssessmentConfig { + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "季度绩效模板") + @NotBlank(message = "模板名称不能为空") + @Size(max = 50, message = "模板名称不能超过 50 个字符") + private String name; + + @Schema(description = "计分方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "计分方式不能为空") + @InEnum(value = HrmPerformanceScoreCalculationEnum.class, message = "计分方式必须是 {value}") + private Integer scoreCalculation; + + @Schema(description = "分数上限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分数上限类型不能为空") + @InEnum(value = HrmPerformanceUpperLimitTypeEnum.class, message = "分数上限类型必须是 {value}") + private Integer upperLimitType; + + @Schema(description = "分数上限", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "分数上限不能为空") + @DecimalMin(value = "0", message = "分数上限不能小于 0") + @DecimalMax(value = "100", message = "分数上限不能大于 100") + private BigDecimal upperLimitScore; + + @Schema(description = "考核维度配置", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "至少需要一个考核维度") + private List<@NotNull(message = "考核维度不能为空") Dimension> dimensions; + + @AssertTrue(message = "考核维度名称不能重复") + @JsonIgnore + public boolean isDimensionNamesUnique() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + Set names = new HashSet<>(); + for (Dimension dimension : dimensions) { + if (dimension != null && StrUtil.isNotBlank(dimension.getName()) + && !names.add(dimension.getName().trim())) { + return false; + } + } + return true; + } + + @AssertTrue(message = "考核指标名称不能重复") + @JsonIgnore + public boolean isQuotaNamesUnique() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + Set names = new HashSet<>(); + for (Dimension dimension : dimensions) { + if (dimension == null || CollUtil.isEmpty(dimension.getQuotas())) { + continue; + } + for (Quota quota : dimension.getQuotas()) { + if (quota != null && StrUtil.isNotBlank(quota.getName()) + && !names.add(quota.getName().trim())) { + return false; + } + } + } + return true; + } + + @AssertTrue(message = "维度权重总和必须等于 100%") + @JsonIgnore + public boolean isDimensionWeightTotalValid() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + BigDecimal totalWeight = BigDecimal.ZERO; + for (Dimension dimension : dimensions) { + if (dimension == null || dimension.getWeight() == null) { + return true; + } + totalWeight = totalWeight.add(dimension.getWeight()); + } + return totalWeight.compareTo(PERCENT_100) == 0; + } + + @AssertTrue(message = "不可编辑维度的指标权重总和必须等于 100%") + @JsonIgnore + public boolean isFixedQuotaWeightTotalValid() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + for (Dimension dimension : dimensions) { + if (dimension == null || ObjUtil.notEqual(Boolean.FALSE, dimension.getAllowEdit()) + || CollUtil.isEmpty(dimension.getQuotas())) { + continue; + } + BigDecimal totalWeight = BigDecimal.ZERO; + for (Quota quota : dimension.getQuotas()) { + if (quota == null || quota.getWeight() == null) { + return true; + } + totalWeight = totalWeight.add(quota.getWeight()); + } + if (totalWeight.compareTo(PERCENT_100) != 0) { + return false; + } + } + return true; + } + + @AssertTrue(message = "可编辑维度的指标权重总和不能大于 100%") + @JsonIgnore + public boolean isEditableQuotaWeightTotalValid() { + if (CollUtil.isEmpty(dimensions)) { + return true; + } + for (Dimension dimension : dimensions) { + if (dimension == null || ObjUtil.notEqual(Boolean.TRUE, dimension.getAllowEdit()) + || CollUtil.isEmpty(dimension.getQuotas())) { + continue; + } + BigDecimal totalWeight = BigDecimal.ZERO; + for (Quota quota : dimension.getQuotas()) { + if (quota == null || quota.getWeight() == null) { + return true; + } + totalWeight = totalWeight.add(quota.getWeight()); + } + if (totalWeight.compareTo(PERCENT_100) > 0) { + return false; + } + } + return true; + } + } + + @Schema(description = "管理后台 - HRM 绩效结果配置快照") + @Data + public static class ResultConfig { + + private static final BigDecimal MIN_SCORE = BigDecimal.ZERO; + private static final BigDecimal MAX_SCORE = BigDecimal.valueOf(100); + private static final BigDecimal SCORE_STEP = new BigDecimal("0.01"); + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "绩效结果模板") + @NotBlank(message = "结果模板名称不能为空") + @Size(max = 255, message = "结果模板名称不能超过 255 个字符") + private String name; + + @Schema(description = "结果等级配置", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "至少需要一个结果等级") + private List<@NotNull(message = "结果等级不能为空") Level> levels; + + @AssertTrue(message = "结果等级名称不能重复") + @JsonIgnore + public boolean isLevelNamesUnique() { + if (CollUtil.isEmpty(levels)) { + return true; + } + Set names = new HashSet<>(); + for (Level level : levels) { + if (level != null && StrUtil.isNotBlank(level.getName()) + && !names.add(level.getName().trim())) { + return false; + } + } + return true; + } + + @AssertTrue(message = "结果等级分数区间必须连续覆盖 0 到 100 分") + @JsonIgnore + public boolean isScoreRangeContinuous() { + if (CollUtil.isEmpty(levels)) { + return true; + } + List sortedLevels = new ArrayList<>(levels); + if (sortedLevels.stream().anyMatch(level -> level == null + || level.getMinScore() == null || level.getMaxScore() == null)) { + return true; + } + sortedLevels.sort(Comparator.comparing(Level::getMinScore)); + if (CollUtil.getFirst(sortedLevels).getMinScore().compareTo(MIN_SCORE) != 0) { + return false; + } + for (int i = 1; i < sortedLevels.size(); i++) { + Level previous = sortedLevels.get(i - 1); + Level current = sortedLevels.get(i); + if (current.getMinScore().compareTo(previous.getMaxScore().add(SCORE_STEP)) != 0) { + return false; + } + } + return CollUtil.getLast(sortedLevels).getMaxScore().compareTo(MAX_SCORE) == 0; + } + } + + // Service 只校验模板、员工和部门等数据引用。 + @AssertTrue(message = "考评范围配置不正确") + @JsonIgnore + public boolean isScopeConfigValid() { + if (CollUtil.isEmpty(scopes)) { + return true; + } + for (Scope scope : scopes) { + if (scope == null || scope.getType() == null) { + continue; + } + if (Objects.equals(scope.getType(), HrmPerformancePlanScopeTypeEnum.EMPLOYEE_DEPT.getType())) { + if (CollUtil.isEmpty(scope.getEmployeeIds()) && CollUtil.isEmpty(scope.getDeptIds())) { + return false; + } + } else if (Objects.equals(scope.getType(), HrmPerformancePlanScopeTypeEnum.EMPLOYMENT.getType())) { + if (scope.getEmployeeType() == null || CollUtil.isEmpty(scope.getEmployeeStatuses())) { + return false; + } + } else if (Objects.equals(scope.getType(), + HrmPerformancePlanScopeTypeEnum.EXCLUDED_EMPLOYEE.getType())) { + return false; + } + } + return true; + } + + @AssertTrue(message = "评分人配置不正确") + @JsonIgnore + public boolean isReviewStageConfigValid() { + if (CollUtil.isEmpty(reviewStages)) { + return true; + } + for (ReviewStage reviewStage : reviewStages) { + if (reviewStage == null || reviewStage.getRater() == null) { + continue; + } + if (!isHandlerStageValid(reviewStage.getRater(), true) + || Objects.equals(reviewStage.getRater().getType(), HrmPerformanceRaterTypeEnum.SELF.getType()) + && Boolean.TRUE.equals(reviewStage.getRejectAuthority())) { + return false; + } + } + return true; + } + + @AssertTrue(message = "评分权重总和必须等于 100%") + @JsonIgnore + public boolean isReviewStageWeightTotalValid() { + if (CollUtil.isEmpty(reviewStages)) { + return true; + } + BigDecimal totalWeight = BigDecimal.ZERO; + for (ReviewStage reviewStage : reviewStages) { + if (reviewStage == null || reviewStage.getWeight() == null) { + return true; + } + totalWeight = totalWeight.add(reviewStage.getWeight()); + } + return totalWeight.compareTo(PERCENT_100) == 0; + } + + @AssertTrue(message = "评分人不能重复") + @JsonIgnore + public boolean isReviewStageRaterUnique() { + if (CollUtil.isEmpty(reviewStages)) { + return true; + } + List raters = new ArrayList<>(); + for (ReviewStage reviewStage : reviewStages) { + if (reviewStage != null) { + raters.add(reviewStage.getRater()); + } + } + return isHandlerStageUnique(raters); + } + + @AssertTrue(message = "目标确认配置不正确") + @JsonIgnore + public boolean isTargetConfirmationConfigValid() { + if (quotaSettingType == null || targetConfirmation == null) { + return true; + } + if (Objects.equals(quotaSettingType, HrmPerformanceQuotaSettingTypeEnum.SYSTEM.getType())) { + return !targetConfirmation; + } + return !targetConfirmation || targetConfirmationStage != null + && isHandlerStageValid(targetConfirmationStage, true); + } + + @AssertTrue(message = "结果审核节点配置不正确") + @JsonIgnore + public boolean isResultAuditConfigValid() { + return ObjUtil.notEqual(Boolean.TRUE, resultAudit) || isHandlerStagesValid(resultAuditStages); + } + + @AssertTrue(message = "申诉节点配置不正确") + @JsonIgnore + public boolean isResultConfirmationConfigValid() { + return ObjUtil.notEqual(Boolean.TRUE, resultConfirmation) || isHandlerStagesValid(appealStages); + } + + @AssertTrue(message = "计薪月份配置不正确") + @JsonIgnore + public boolean isSalaryConfigValid() { + if (!Boolean.TRUE.equals(syncToSalary)) { + return true; + } + if (StrUtil.isBlank(paidForMonth)) { + return false; + } + try { + YearMonth.parse(paidForMonth); + return true; + } catch (DateTimeParseException ignored) { + return false; + } + } + + @AssertTrue(message = "考核周期、季度和起止时间配置不正确") + @JsonIgnore + public boolean isCycleConfigValid() { + if (cycleType == null) { + return true; + } + if (startTime == null || endTime == null || endTime.isBefore(startTime)) { + return false; + } + if (Objects.equals(cycleType, HrmPerformanceCycleTypeEnum.OTHER.getType())) { + return StrUtil.isNotBlank(cycle) && quarter == null; + } + try { + if (Objects.equals(cycleType, HrmPerformanceCycleTypeEnum.MONTH.getType())) { + YearMonth month = YearMonth.parse(cycle); + return quarter == null && startTime.toLocalDate().equals(month.atDay(1)) + && endTime.toLocalDate().equals(month.atEndOfMonth()); + } + Year year = Year.parse(cycle); + int beginMonth = 1; + int endMonth = 12; + if (Objects.equals(cycleType, HrmPerformanceCycleTypeEnum.QUARTER.getType())) { + if (HrmPerformanceQuarterEnum.valueOf(quarter) == null) { + return false; + } + beginMonth = (quarter - 1) * 3 + 1; + endMonth = beginMonth + 2; + } else if (Objects.equals(cycleType, + HrmPerformanceCycleTypeEnum.FIRST_HALF_YEAR.getType())) { + endMonth = 6; + } else if (Objects.equals(cycleType, + HrmPerformanceCycleTypeEnum.SECOND_HALF_YEAR.getType())) { + beginMonth = 7; + } + if (!Objects.equals(cycleType, HrmPerformanceCycleTypeEnum.QUARTER.getType()) + && quarter != null) { + return false; + } + YearMonth beginYearMonth = year.atMonth(beginMonth); + YearMonth endYearMonth = year.atMonth(endMonth); + return startTime.toLocalDate().equals(beginYearMonth.atDay(1)) + && endTime.toLocalDate().equals(endYearMonth.atEndOfMonth()); + } catch (DateTimeParseException | NullPointerException ignored) { + return false; + } + } + + private boolean isHandlerStagesValid(List stages) { + if (CollUtil.isEmpty(stages) || stages.size() > 3) { + return false; + } + for (HandlerStage stage : stages) { + if (stage != null && !isHandlerStageValid(stage, false)) { + return false; + } + } + return isHandlerStageUnique(stages); + } + + private boolean isHandlerStageValid(HandlerStage stage, boolean allowSelf) { + if (stage == null || stage.getType() == null) { + return true; + } + if (Objects.equals(stage.getType(), HrmPerformanceRaterTypeEnum.SELF.getType())) { + return allowSelf; + } + if (ObjectUtils.equalsAny(stage.getType(), HrmPerformanceRaterTypeEnum.SUPERIOR.getType(), + HrmPerformanceRaterTypeEnum.DEPT_LEADER.getType())) { + return stage.getLevel() != null && stage.getLevel() >= 1 && stage.getLevel() <= 10; + } + if (Objects.equals(stage.getType(), HrmPerformanceRaterTypeEnum.SPECIFIED.getType())) { + return stage.getEmployeeId() != null; + } + return true; + } + + private boolean isHandlerStageUnique(List stages) { + if (CollUtil.isEmpty(stages)) { + return true; + } + Set handlerKeys = new HashSet<>(); + for (HandlerStage stage : stages) { + String handlerKey = buildHandlerKey(stage); + if (handlerKey != null && !handlerKeys.add(handlerKey)) { + return false; + } + } + return true; + } + + private String buildHandlerKey(HandlerStage stage) { + if (stage == null || stage.getType() == null) { + return null; + } + if (Objects.equals(stage.getType(), HrmPerformanceRaterTypeEnum.SELF.getType())) { + return String.valueOf(stage.getType()); + } + if (Objects.equals(stage.getType(), HrmPerformanceRaterTypeEnum.SPECIFIED.getType())) { + return stage.getEmployeeId() == null ? null : stage.getType() + ":" + stage.getEmployeeId(); + } + if (ObjectUtils.equalsAny(stage.getType(), HrmPerformanceRaterTypeEnum.SUPERIOR.getType(), + HrmPerformanceRaterTypeEnum.DEPT_LEADER.getType())) { + return stage.getLevel() == null ? null : stage.getType() + ":" + stage.getLevel(); + } + return null; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplatePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplatePageReqVO.java new file mode 100644 index 0000000..3b3b4e9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplatePageReqVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.resulttemplate; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 绩效结果模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPerformanceResultTemplatePageReqVO extends PageParam { + + @Schema(description = "结果模板名称,模糊匹配", example = "季度") + private String name; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplateRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplateRespVO.java new file mode 100644 index 0000000..109c9e9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplateRespVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.resulttemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 绩效结果模板 Response VO") +@Data +public class HrmPerformanceResultTemplateRespVO { + + @Schema(description = "结果模板编号", example = "1024") + private Long id; + + @Schema(description = "结果模板名称", example = "季度绩效结果") + private String name; + + @Schema(description = "状态", example = "0") + private Integer status; + + @Schema(description = "结果等级列表") + private List levels; + + @Schema(description = "创建人") + private String creator; + + @Schema(description = "创建人名称") + private String creatorName; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "管理后台 - HRM 绩效结果模板等级") + @Data + public static class Level { + + @Schema(description = "等级名称", example = "A") + private String name; + + @Schema(description = "最低分数", example = "90") + private BigDecimal minScore; + + @Schema(description = "最高分数", example = "100") + private BigDecimal maxScore; + + @Schema(description = "绩效系数", example = "1.2") + private BigDecimal coefficient; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplateSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplateSaveReqVO.java new file mode 100644 index 0000000..968d823 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performance/vo/resulttemplate/HrmPerformanceResultTemplateSaveReqVO.java @@ -0,0 +1,129 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.resulttemplate; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Schema(description = "管理后台 - HRM 绩效结果模板保存 Request VO") +@Data +public class HrmPerformanceResultTemplateSaveReqVO { + + private static final BigDecimal MIN_SCORE = BigDecimal.ZERO; + private static final BigDecimal MAX_SCORE = BigDecimal.valueOf(100); + private static final BigDecimal SCORE_STEP = new BigDecimal("0.01"); + + @Schema(description = "结果模板编号", example = "1024") + private Long id; + + @Schema(description = "结果模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "季度绩效结果") + @NotBlank(message = "结果模板名称不能为空") + @Size(max = 255, message = "结果模板名称不能超过 255 个字符") + private String name; + + @Schema(description = "结果等级列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "至少需要一个结果等级") + private List<@NotNull(message = "结果等级不能为空") Level> levels; + + /** + * 校验结果等级名称唯一 + */ + @AssertTrue(message = "结果等级名称不能重复") + @JsonIgnore + public boolean isLevelNamesUnique() { + if (CollUtil.isEmpty(levels)) { + return true; + } + Set names = new HashSet<>(); + for (Level level : levels) { + if (level != null && StrUtil.isNotBlank(level.getName()) + && !names.add(level.getName().trim())) { + return false; + } + } + return true; + } + + /** + * 校验结果等级分数区间连续覆盖 0 到 100 分 + */ + @AssertTrue(message = "结果等级分数区间必须连续覆盖 0 到 100 分") + @JsonIgnore + public boolean isScoreRangeContinuous() { + if (CollUtil.isEmpty(levels)) { + return true; + } + List sortedLevels = new ArrayList<>(levels); + if (sortedLevels.stream().anyMatch(level -> level == null + || level.getMinScore() == null || level.getMaxScore() == null)) { + return true; + } + sortedLevels.sort(Comparator.comparing(Level::getMinScore)); + if (CollUtil.getFirst(sortedLevels).getMinScore().compareTo(MIN_SCORE) != 0) { + return false; + } + for (int i = 1; i < sortedLevels.size(); i++) { + Level previous = sortedLevels.get(i - 1); + Level current = sortedLevels.get(i); + if (current.getMinScore().compareTo(previous.getMaxScore().add(SCORE_STEP)) != 0) { + return false; + } + } + return CollUtil.getLast(sortedLevels).getMaxScore().compareTo(MAX_SCORE) == 0; + } + + @Schema(description = "管理后台 - HRM 绩效结果模板等级") + @Data + public static class Level { + + @Schema(description = "等级名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "A") + @NotBlank(message = "等级名称不能为空") + @Size(max = 255, message = "等级名称不能超过 255 个字符") + private String name; + + @Schema(description = "最低分数", requiredMode = Schema.RequiredMode.REQUIRED, example = "90") + @NotNull(message = "最低分数不能为空") + @DecimalMin(value = "0", message = "最低分数不能小于 0") + @DecimalMax(value = "100", message = "最低分数不能大于 100") + @Digits(integer = 3, fraction = 2, message = "最低分数最多保留两位小数") + private BigDecimal minScore; + + @Schema(description = "最高分数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "最高分数不能为空") + @DecimalMin(value = "0", message = "最高分数不能小于 0") + @DecimalMax(value = "100", message = "最高分数不能大于 100") + @Digits(integer = 3, fraction = 2, message = "最高分数最多保留两位小数") + private BigDecimal maxScore; + + @Schema(description = "绩效系数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.2") + @NotNull(message = "绩效系数不能为空") + @DecimalMin(value = "0", message = "绩效系数不能小于 0") + @Digits(integer = 8, fraction = 2, message = "绩效系数最多保留两位小数") + private BigDecimal coefficient; + + @AssertTrue(message = "最低分数不能大于最高分数") + @JsonIgnore + public boolean isScoreRangeValid() { + return minScore == null || maxScore == null || minScore.compareTo(maxScore) <= 0; + } + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceClockController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceClockController.java new file mode 100644 index 0000000..fd8e47a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceClockController.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance.vo.clock.HrmPortalAttendanceClockCreateReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance.vo.clock.HrmPortalAttendanceClockDetailRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance.vo.clock.HrmPortalAttendanceRecordRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceGroupDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record.HrmAttendanceClockDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.service.attendance.record.HrmAttendanceClockService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端考勤打卡") +@RestController +@RequestMapping("/hrm/portal/attendance/clock") +@Validated +public class HrmPortalAttendanceClockController { + + private static final DateTimeFormatter SHIFT_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmAttendanceClockService attendanceClockService; + + @GetMapping("/get-detail") + @Operation(summary = "获得我的打卡详情") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getAttendanceClockDetail() { + HrmPortalAttendanceClockDetailRespVO detail = + attendanceClockService.getMyAttendanceClockDetail(getLoginUserId()); + return success(buildAttendanceClockDetailRespVO(detail)); + } + + @PostMapping("/create") + @Operation(summary = "手机端打卡") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult createAttendanceClock( + @Valid @RequestBody HrmPortalAttendanceClockCreateReqVO createReqVO) { + return success(attendanceClockService.createMyAttendanceClock(getLoginUserId(), createReqVO)); + } + + @GetMapping("/list") + @Operation(summary = "获得我的考勤记录") + @Parameters({ + @Parameter(name = "year", description = "年份", example = "2026"), + @Parameter(name = "month", description = "月份", example = "8") + }) + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getAttendanceRecordList( + @RequestParam(value = "year", required = false) Integer year, + @RequestParam(value = "month", required = false) Integer month) { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + // 1. 计算所选月份的闭区间 + YearMonth queryMonth = year == null || month == null + ? YearMonth.now() : YearMonth.of(year, month); + LocalDateTime[] monthTimes = LocalDateTimeUtils.getMonthDateTimeRange( + queryMonth.getYear(), queryMonth.getMonthValue()); + // 2. 查询当前员工在所选月份的打卡记录 + List list = attendanceClockService.getAttendanceClockListByEmployeeIdAndClockTime( + employee.getId(), monthTimes); + return success(BeanUtils.toBean(list, HrmPortalAttendanceRecordRespVO.class)); + } + + // ==================== 拼接 VO ==================== + + private HrmPortalAttendanceClockDetailRespVO buildAttendanceClockDetailRespVO( + HrmPortalAttendanceClockDetailRespVO detail) { + HrmAttendanceGroupDO group = detail.getGroup(); + detail.setGroupName(group.getName()) + .setOpenPointCard(Boolean.TRUE.equals(group.getOpenPointCard())) + .setOpenWifiCard(Boolean.TRUE.equals(group.getOpenWifiCard())) + .setShiftTitle(buildShiftTitle(detail.getShift())) + .setPoints(Boolean.TRUE.equals(group.getOpenPointCard()) + ? BeanUtils.toBean(CollUtil.emptyIfNull(group.getPoints()), + HrmPortalAttendanceClockDetailRespVO.Point.class) : Collections.emptyList()) + .setWifis(Boolean.TRUE.equals(group.getOpenWifiCard()) + ? BeanUtils.toBean(CollUtil.emptyIfNull(group.getWifis()), + HrmPortalAttendanceClockDetailRespVO.Wifi.class) : Collections.emptyList()); + return detail; + } + + /** + * 构建班次展示标题 + * + * @param shift 班次配置 + * @return 班次标题 + */ + private String buildShiftTitle(HrmAttendanceGroupDO.Shift shift) { + if (shift == null || shift.getStartTime() == null || shift.getEndTime() == null) { + return "休息"; + } + String title = shift.getStartTime().format(SHIFT_TIME_FORMATTER) + + "-" + shift.getEndTime().format(SHIFT_TIME_FORMATTER); + if (shift.getRestStartTime() != null && shift.getRestEndTime() != null) { + title = title + " 休息:" + shift.getRestStartTime().format(SHIFT_TIME_FORMATTER) + + "-" + shift.getRestEndTime().format(SHIFT_TIME_FORMATTER); + } + return title; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceLeaveController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceLeaveController.java new file mode 100644 index 0000000..f122401 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceLeaveController.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeaveCancelReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeaveCreateReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave.HrmAttendanceLeaveRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record.HrmAttendanceLeaveDO; +import cn.iocoder.yudao.module.hrm.service.attendance.record.HrmAttendanceLeaveService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端请假") +@RestController +@RequestMapping("/hrm/portal/attendance/leave") +@Validated +public class HrmPortalAttendanceLeaveController { + + @Resource + private HrmAttendanceLeaveService attendanceLeaveService; + + @GetMapping("/list") + @Operation(summary = "获得我的请假申请列表") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getAttendanceLeaveList() { + List list = attendanceLeaveService.getMyAttendanceLeaveList(getLoginUserId()); + return success(BeanUtils.toBean(list, HrmAttendanceLeaveRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建我的请假申请") + @PreAuthorize("@ss.hasPermission('hrm:portal:attendance:leave')") + public CommonResult createAttendanceLeave( + @Valid @RequestBody HrmAttendanceLeaveCreateReqVO reqVO) { + return success(attendanceLeaveService.createLeave(getLoginUserId(), reqVO)); + } + + @PutMapping("/cancel") + @Operation(summary = "取消我的请假申请") + @PreAuthorize("@ss.hasPermission('hrm:portal:attendance:leave')") + public CommonResult cancelAttendanceLeave( + @Valid @RequestBody HrmAttendanceLeaveCancelReqVO reqVO) { + attendanceLeaveService.cancelLeave(getLoginUserId(), reqVO); + return success(true); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceStatisticsController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceStatisticsController.java new file mode 100644 index 0000000..51d4958 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/HrmPortalAttendanceStatisticsController.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceDailyDetailRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthDetailRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.service.attendance.statistics.HrmAttendanceStatisticsService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.YearMonth; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端考勤统计") +@RestController +@RequestMapping("/hrm/portal/attendance/statistics") +@Validated +public class HrmPortalAttendanceStatisticsController { + + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmAttendanceStatisticsService attendanceStatisticsService; + + @GetMapping("/month-detail") + @Operation(summary = "获得我的月度考勤详情") + @Parameters({ + @Parameter(name = "year", description = "年份", example = "2026"), + @Parameter(name = "month", description = "月份", example = "8") + }) + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getAttendanceMonthDetail( + @RequestParam(value = "year", required = false) Integer year, + @RequestParam(value = "month", required = false) Integer month) { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + YearMonth queryMonth = year == null || month == null + ? YearMonth.now() : YearMonth.of(year, month); + return success(attendanceStatisticsService.getAttendanceMonthDetail( + employee.getId(), queryMonth.getYear(), queryMonth.getMonthValue())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出我的月度考勤日报") + @Parameters({ + @Parameter(name = "year", description = "年份", required = true, example = "2026"), + @Parameter(name = "month", description = "月份", required = true, example = "8") + }) + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public void exportAttendanceMonthDetail( + @RequestParam("year") Integer year, @RequestParam("month") Integer month, + HttpServletResponse response) throws Exception { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + HrmAttendanceMonthDetailRespVO detail = attendanceStatisticsService.getAttendanceMonthDetail( + employee.getId(), year, month); + ExcelUtils.write(response, year + "年" + month + "月个人考勤日报.xls", "考勤日报", + HrmAttendanceDailyDetailRespVO.class, detail.getDailyDetails()); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceClockCreateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceClockCreateReqVO.java new file mode 100644 index 0000000..8a7b6d2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceClockCreateReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance.vo.clock; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工端手机打卡 Request VO") +@Data +public class HrmPortalAttendanceClockCreateReqVO { + + @Schema(description = "打卡地址", example = "上海市浦东新区") + @Size(max = 255, message = "打卡地址不能超过 255 个字符") + private String address; + + @Schema(description = "经度", example = "121.473701") + @DecimalMin(value = "-180", message = "经度必须在 -180 到 180 之间") + @DecimalMax(value = "180", message = "经度必须在 -180 到 180 之间") + private BigDecimal longitude; + + @Schema(description = "纬度", example = "31.230416") + @DecimalMin(value = "-90", message = "纬度必须在 -90 到 90 之间") + @DecimalMax(value = "90", message = "纬度必须在 -90 到 90 之间") + private BigDecimal latitude; + + @Schema(description = "WiFi 名称", example = "office_wifi") + @Size(max = 50, message = "WiFi 名称不能超过 50 个字符") + private String ssid; + + @Schema(description = "WiFi MAC 地址", example = "00:11:22:33:44:55") + @Size(max = 50, message = "WiFi MAC 地址不能超过 50 个字符") + private String mac; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceClockDetailRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceClockDetailRespVO.java new file mode 100644 index 0000000..6dca4b0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceClockDetailRespVO.java @@ -0,0 +1,147 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance.vo.clock; + +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange; +import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config.HrmAttendanceGroupDO; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端打卡详情 Response VO") +@Data +public class HrmPortalAttendanceClockDetailRespVO { + + @Schema(description = "考勤组名称") + private String groupName; + + @Schema(description = "是否启用定位打卡") + private Boolean openPointCard; + + @Schema(description = "是否启用 WiFi 打卡") + private Boolean openWifiCard; + + @Schema(description = "班次所属考勤日") + private LocalDate attendanceDate; + + @Schema(description = "班次标题,例如 09:00-18:00") + private String shiftTitle; + + @Schema(description = "是否休息日") + private Boolean restDay; + + @Schema(description = "上班应打卡时间") + private LocalDateTime onDutyAttendanceTime; + + @Schema(description = "下班应打卡时间") + private LocalDateTime offDutyAttendanceTime; + + @Schema(description = "打卡地点列表") + private List points; + + @Schema(description = "WiFi 列表") + private List wifis; + + @Schema(description = "下一次可打卡动作") + private NextClock nextClock; + + @Schema(description = "当日打卡时间线") + private List timeline; + + // ==================== 内部计算字段 ==================== + + @JsonIgnore + @Schema(hidden = true) + private HrmAttendanceGroupDO group; + + @JsonIgnore + @Schema(hidden = true) + private HrmAttendanceGroupDO.Shift shift; + + @JsonIgnore + @Schema(hidden = true) + private TimeRange clockInRange; + + @JsonIgnore + @Schema(hidden = true) + private TimeRange clockOutRange; + + @Schema(description = "打卡地点") + @Data + public static class Point { + + @Schema(description = "地点名称") + private String name; + + @Schema(description = "定位地址") + private String address; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "有效半径(米)") + private Integer radius; + + } + + @Schema(description = "WiFi 配置") + @Data + public static class Wifi { + + @Schema(description = "WiFi 名称") + private String ssid; + + @Schema(description = "MAC 地址") + private String mac; + + } + + @Schema(description = "下一次打卡动作") + @Data + public static class NextClock { + + @Schema(description = "打卡类型") + private Integer type; + + @Schema(description = "打卡阶段") + private Integer stage; + + @Schema(description = "应打卡时间") + private LocalDateTime attendanceTime; + + @Schema(description = "按钮状态:0 未到时间 1 正常 2 更新 3 迟到 4 早退") + private Integer buttonStatus; + + } + + @Schema(description = "当日打卡时间线条目") + @Data + public static class TimelineItem { + + @Schema(description = "打卡类型") + private Integer type; + + @Schema(description = "应打卡时间") + private LocalDateTime attendanceTime; + + @Schema(description = "实际打卡时间;缺卡时为空") + private LocalDateTime clockTime; + + @Schema(description = "打卡状态") + private Integer status; + + @Schema(description = "是否缺卡") + private Boolean missCard; + + @Schema(description = "打卡地址") + private String address; + + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceRecordRespVO.java new file mode 100644 index 0000000..e63f4bc --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/attendance/vo/clock/HrmPortalAttendanceRecordRespVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.attendance.vo.clock; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工端考勤记录 Response VO") +@Data +public class HrmPortalAttendanceRecordRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "打卡时间") + private LocalDateTime clockTime; + + @Schema(description = "员工端考勤记录类型") + private Integer type; + + @Schema(description = "应打卡时间") + private LocalDateTime attendanceTime; + + @Schema(description = "来源类型") + private Integer sourceType; + + @Schema(description = "员工端考勤记录状态") + private Integer status; + + @Schema(description = "阶段") + private Integer stage; + + @Schema(description = "地址") + private String address; + + @Schema(description = "经度") + private BigDecimal longitude; + + @Schema(description = "纬度") + private BigDecimal latitude; + + @Schema(description = "WiFi 名称") + private String ssid; + + @Schema(description = "WiFi MAC 地址") + private String mac; + + @Schema(description = "备注") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeCertificateController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeCertificateController.java new file mode 100644 index 0000000..9f0702c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeCertificateController.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.certificate.HrmEmployeeCertificateRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeCertificateDO; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeCertificateService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端证书") +@RestController +@RequestMapping("/hrm/portal/employee/certificate") +@Validated +public class HrmPortalEmployeeCertificateController { + + @Resource + private HrmEmployeeCertificateService certificateService; + @Resource + private HrmEmployeeService employeeService; + + @GetMapping("/list") + @Operation(summary = "获得我的证书列表") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getCertificateList() { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + List certificates = + certificateService.getCertificateListByEmployeeId(employeeId); + return success(BeanUtils.toBean(certificates, HrmEmployeeCertificateRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeContactController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeContactController.java new file mode 100644 index 0000000..ec941b3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeContactController.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.contact.HrmEmployeeContactRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeContactDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeContactService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端联系人") +@RestController +@RequestMapping("/hrm/portal/employee/contact") +@Validated +public class HrmPortalEmployeeContactController { + + @Resource + private HrmEmployeeContactService contactService; + @Resource + private HrmEmployeeService employeeService; + + @GetMapping("/list") + @Operation(summary = "获得我的联系人列表") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getContactList() { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + List contacts = contactService.getContactListByEmployeeId(employeeId); + return success(BeanUtils.toBean(contacts, HrmEmployeeContactRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeController.java new file mode 100644 index 0000000..d4e770f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeController.java @@ -0,0 +1,142 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.employee.vo.employee.HrmPortalEmployeeRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.employee.vo.employee.HrmPortalEmployeeUpdateReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.employee.config.HrmEmployeeArchiveFieldEnum; +import cn.iocoder.yudao.module.hrm.service.employee.config.HrmEmployeeFieldConfigService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.getYearsBetween; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端档案") +@RestController +@RequestMapping("/hrm/portal/employee") +@Validated +public class HrmPortalEmployeeController { + + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmEmployeeFieldConfigService employeeFieldConfigService; + @Resource + private DeptApi deptApi; + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/get-bind-status") + @Operation(summary = "获得当前账号的员工绑定状态") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getEmployeeBindStatus() { + return success(employeeService.getEmployeeByUserId(getLoginUserId()) != null); + } + + @GetMapping("/get") + @Operation(summary = "获得我的员工档案") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getEmployee() { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + // 拼接 VO + HrmPortalEmployeeRespVO respVO = BeanUtils.toBean(employee, HrmPortalEmployeeRespVO.class); + LocalDate today = LocalDate.now(); + if (employee.getBirthday() != null) { + respVO.setAge(getYearsBetween(employee.getBirthday().toLocalDate(), today)); + } + if (employee.getCompanyAgeStartTime() != null) { + respVO.setCompanyAge(getYearsBetween(employee.getCompanyAgeStartTime().toLocalDate(), today)); + } + respVO.setEntryDay(employee.getEntryTime() == null ? 0L + : Math.max(0L, ChronoUnit.DAYS.between(employee.getEntryTime().toLocalDate(), today) + 1)); + if (employee.getUserId() != null) { + AdminUserRespDTO user = adminUserApi.getUser(employee.getUserId()); + respVO.setAvatar(user == null ? null : user.getAvatar()); + } + if (employee.getDeptId() != null) { + DeptRespDTO dept = deptApi.getDept(employee.getDeptId()); + respVO.setDeptName(dept == null ? null : dept.getName()); + } + if (employee.getLeaderEmployeeId() != null) { + HrmEmployeeDO leader = employeeService.getEmployee(employee.getLeaderEmployeeId()); + respVO.setLeaderEmployeeName(leader == null ? null : leader.getName()); + } + hideInvisibleArchiveFields(respVO, employeeFieldConfigService.getVisibleArchiveFieldNames()); + return success(respVO); + } + + @PutMapping("/update") + @Operation(summary = "更新我的员工档案") + @PreAuthorize("@ss.hasPermission('hrm:portal:employee:update')") + public CommonResult updateEmployee( + @Valid @RequestBody HrmPortalEmployeeUpdateReqVO reqVO) { + employeeService.updateEmployeeBySelf(getLoginUserId(), reqVO); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private void hideInvisibleArchiveFields(HrmPortalEmployeeRespVO respVO, Set visibleFields) { + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.NAME.getName())) { + respVO.setName(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.COUNTRY.getName())) { + respVO.setCountry(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.NATION.getName())) { + respVO.setNation(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.ID_TYPE.getName())) { + respVO.setIdType(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.ID_NUMBER.getName())) { + respVO.setIdNumber(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.SEX.getName())) { + respVO.setSex(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.NATIVE_PLACE.getName())) { + respVO.setNativePlace(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.BIRTHDAY.getName())) { + respVO.setBirthday(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.AGE.getName())) { + respVO.setAge(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.HIGHEST_EDUCATION.getName())) { + respVO.setHighestEducation(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.MOBILE.getName())) { + respVO.setMobile(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.EMAIL.getName())) { + respVO.setEmail(null); + } + if (!visibleFields.contains(HrmEmployeeArchiveFieldEnum.ADDRESS.getName())) { + respVO.setAddress(null); + } + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeEducationExperienceController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeEducationExperienceController.java new file mode 100644 index 0000000..fc8cec3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeEducationExperienceController.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.educationexperience.HrmEmployeeEducationExperienceRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeEducationExperienceDO; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeEducationExperienceService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端教育经历") +@RestController +@RequestMapping("/hrm/portal/employee/education-experience") +@Validated +public class HrmPortalEmployeeEducationExperienceController { + + @Resource + private HrmEmployeeEducationExperienceService educationExperienceService; + @Resource + private HrmEmployeeService employeeService; + + @GetMapping("/list") + @Operation(summary = "获得我的教育经历列表") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getEducationExperienceList() { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + List educationExperiences = + educationExperienceService.getEducationExperienceListByEmployeeId(employeeId); + return success(BeanUtils.toBean( + educationExperiences, HrmEmployeeEducationExperienceRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeFieldConfigController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeFieldConfigController.java new file mode 100644 index 0000000..eb00fb9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeFieldConfigController.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.config.HrmEmployeeFieldConfigRespVO; +import cn.iocoder.yudao.module.hrm.service.employee.config.HrmEmployeeFieldConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 员工端档案字段配置") +@RestController +@RequestMapping("/hrm/portal/employee/field-config") +@Validated +public class HrmPortalEmployeeFieldConfigController { + + @Resource + private HrmEmployeeFieldConfigService employeeFieldConfigService; + + @GetMapping("/list") + @Operation(summary = "获得我的员工档案字段配置") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getEmployeeFieldConfigList() { + return success(employeeFieldConfigService.getEmployeeArchiveFieldConfigList()); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeQuitInfoController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeQuitInfoController.java new file mode 100644 index 0000000..1fae541 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeQuitInfoController.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.quitinfo.HrmEmployeeQuitInfoRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeQuitInfoDO; +import cn.iocoder.yudao.module.hrm.service.employee.employment.HrmEmployeeQuitInfoService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端离职信息") +@RestController +@RequestMapping("/hrm/portal/employee/quit-info") +@Validated +public class HrmPortalEmployeeQuitInfoController { + + @Resource + private HrmEmployeeQuitInfoService quitInfoService; + @Resource + private HrmEmployeeService employeeService; + + @GetMapping("/get") + @Operation(summary = "获得我的离职信息") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getQuitInfo() { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + HrmEmployeeQuitInfoDO quitInfo = quitInfoService.getQuitInfoByEmployeeId(employeeId); + return success(BeanUtils.toBean(quitInfo, HrmEmployeeQuitInfoRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeTrainingExperienceController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeTrainingExperienceController.java new file mode 100644 index 0000000..e67400f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeTrainingExperienceController.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.trainingexperience.HrmEmployeeTrainingExperienceRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeTrainingExperienceDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeTrainingExperienceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端培训经历") +@RestController +@RequestMapping("/hrm/portal/employee/training-experience") +@Validated +public class HrmPortalEmployeeTrainingExperienceController { + + @Resource + private HrmEmployeeTrainingExperienceService trainingExperienceService; + @Resource + private HrmEmployeeService employeeService; + + @GetMapping("/list") + @Operation(summary = "获得我的培训经历列表") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getTrainingExperienceList() { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + List trainingExperiences = + trainingExperienceService.getTrainingExperienceListByEmployeeId(employeeId); + return success(BeanUtils.toBean( + trainingExperiences, HrmEmployeeTrainingExperienceRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeWorkExperienceController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeWorkExperienceController.java new file mode 100644 index 0000000..aa8c4b9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/HrmPortalEmployeeWorkExperienceController.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.workexperience.HrmEmployeeWorkExperienceRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience.HrmEmployeeWorkExperienceDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.employee.experience.HrmEmployeeWorkExperienceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端工作经历") +@RestController +@RequestMapping("/hrm/portal/employee/work-experience") +@Validated +public class HrmPortalEmployeeWorkExperienceController { + + @Resource + private HrmEmployeeWorkExperienceService workExperienceService; + @Resource + private HrmEmployeeService employeeService; + + @GetMapping("/list") + @Operation(summary = "获得我的工作经历列表") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getWorkExperienceList() { + Long employeeId = employeeService.validateEmployeeBySelf(getLoginUserId()).getId(); + List workExperiences = + workExperienceService.getWorkExperienceListByEmployeeId(employeeId); + return success(BeanUtils.toBean( + workExperiences, HrmEmployeeWorkExperienceRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/vo/employee/HrmPortalEmployeeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/vo/employee/HrmPortalEmployeeRespVO.java new file mode 100644 index 0000000..c803184 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/vo/employee/HrmPortalEmployeeRespVO.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee.vo.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工端员工档案 Response VO") +@Data +public class HrmPortalEmployeeRespVO { + + @Schema(description = "员工档案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "员工姓名", example = "张三") + private String name; + + @Schema(description = "员工头像") + private String avatar; + + @Schema(description = "工号", example = "HRM001") + private String jobNumber; + + @Schema(description = "手机号", example = "15601691300") + private String mobile; + + @Schema(description = "国家或地区", example = "中国") + private String country; + + @Schema(description = "民族", example = "汉族") + private String nation; + + @Schema(description = "证件类型", example = "1") + private Integer idType; + + @Schema(description = "证件号码", example = "310101199001011234") + private String idNumber; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "邮箱", example = "hrm@example.com") + private String email; + + @Schema(description = "籍贯", example = "浙江杭州") + private String nativePlace; + + @Schema(description = "出生日期") + private LocalDateTime birthday; + + @Schema(description = "年龄", example = "28") + private Integer age; + + @Schema(description = "户籍地址", example = "杭州市西湖区") + private String address; + + @Schema(description = "最高学历", example = "8") + private Integer highestEducation; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门名称", example = "研发部") + private String deptName; + + @Schema(description = "直属上级员工编号", example = "1") + private Long leaderEmployeeId; + + @Schema(description = "直属上级员工姓名", example = "李四") + private String leaderEmployeeName; + + @Schema(description = "入职状态", example = "1") + private Integer entryStatus; + + @Schema(description = "员工状态", example = "1") + private Integer status; + + @Schema(description = "聘用形式", example = "1") + private Integer type; + + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + @Schema(description = "入职天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long entryDay; + + @Schema(description = "试用期,单位月;0 表示无试用期", example = "3") + private Integer probation; + + @Schema(description = "转正时间") + private LocalDateTime regularTime; + + @Schema(description = "离职时间") + private LocalDateTime leaveTime; + + @Schema(description = "职位名称", example = "Java 工程师") + private String postName; + + @Schema(description = "岗位职级", example = "P6") + private String postLevel; + + @Schema(description = "工作城市", example = "上海") + private String workCity; + + @Schema(description = "工作地点", example = "浦东新区") + private String workAddress; + + @Schema(description = "工作详细地址", example = "张江高科 1 号楼") + private String workDetailAddress; + + @Schema(description = "司龄开始时间") + private LocalDateTime companyAgeStartTime; + + @Schema(description = "司龄,单位年", example = "2") + private Integer companyAge; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/vo/employee/HrmPortalEmployeeUpdateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/vo/employee/HrmPortalEmployeeUpdateReqVO.java new file mode 100644 index 0000000..3c541ba --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/employee/vo/employee/HrmPortalEmployeeUpdateReqVO.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.employee.vo.employee; + +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.hrm.enums.employee.experience.HrmEmployeeEducationEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeIdTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工端档案更新 Request VO") +@Data +public class HrmPortalEmployeeUpdateReqVO { + + @Schema(description = "员工姓名", example = "张三") + @Size(max = 255, message = "员工姓名不能超过 255 个字符") + private String name; + + @Schema(description = "手机号", example = "15601691300") + @Mobile + private String mobile; + + @Schema(description = "国家或地区", example = "中国") + @Size(max = 64, message = "国家或地区不能超过 64 个字符") + private String country; + + @Schema(description = "民族", example = "汉族") + @Size(max = 64, message = "民族不能超过 64 个字符") + private String nation; + + @Schema(description = "证件类型", example = "1") + @InEnum(value = HrmEmployeeIdTypeEnum.class, message = "证件类型必须是 {value}") + private Integer idType; + + @Schema(description = "证件号码", example = "310101199001011234") + @Size(max = 255, message = "证件号码不能超过 255 个字符") + private String idNumber; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "邮箱", example = "hrm@example.com") + @Email(message = "邮箱格式不正确") + @Size(max = 255, message = "邮箱不能超过 255 个字符") + private String email; + + @Schema(description = "籍贯", example = "浙江杭州") + @Size(max = 128, message = "籍贯不能超过 128 个字符") + private String nativePlace; + + @Schema(description = "出生日期") + @PastOrPresent(message = "出生日期不能晚于当前时间") + private LocalDateTime birthday; + + @Schema(description = "户籍地址", example = "杭州市西湖区") + @Size(max = 255, message = "户籍地址不能超过 255 个字符") + private String address; + + @Schema(description = "最高学历", example = "8") + @InEnum(value = HrmEmployeeEducationEnum.class, message = "最高学历必须是 {value}") + private Integer highestEducation; + + @AssertTrue(message = "身份证号码格式不正确") + @JsonIgnore + public boolean isIdNumberValid() { + return ObjUtil.notEqual(HrmEmployeeIdTypeEnum.ID_CARD.getType(), idType) + || StrUtil.isBlank(idNumber) || IdcardUtil.isValidCard(idNumber); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/home/HrmPortalHomeController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/home/HrmPortalHomeController.java new file mode 100644 index 0000000..e6ebe27 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/home/HrmPortalHomeController.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.home; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.hrm.controller.admin.home.vo.HrmHomeCalendarItemRespVO; +import cn.iocoder.yudao.module.hrm.service.home.HrmHomeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.util.List; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端首页") +@RestController +@RequestMapping("/hrm/portal/home") +@Validated +public class HrmPortalHomeController { + + @Resource + private HrmHomeService homeService; + + @GetMapping("/calendar") + @Operation(summary = "获得员工端首页日历") + @Parameters({ + @Parameter(name = "startDate", description = "开始日期", required = true, example = "2026-08-01"), + @Parameter(name = "endDate", description = "结束日期", required = true, example = "2026-08-31") + }) + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getEmployeeHomeCalendar( + @RequestParam("startDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam("endDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + return success(homeService.getEmployeeCalendar(getLoginUserId(), startDate, endDate)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/HrmPortalInsuranceController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/HrmPortalInsuranceController.java new file mode 100644 index 0000000..c76b3ef --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/HrmPortalInsuranceController.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.insurance; + +import cn.hutool.core.util.BooleanUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.insurance.vo.HrmPortalInsuranceRecordRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.monthrecord.HrmInsuranceMonthEmployeeRecordDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.insurance.monthrecord.HrmInsuranceMonthEmployeeRecordService; +import cn.iocoder.yudao.module.hrm.service.insurance.config.HrmInsuranceSchemeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.hutool.core.util.ObjectUtil.notEqual; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.INSURANCE_MONTH_EMP_RECORD_NOT_EXISTS; + +@Tag(name = "管理后台 - HRM 员工端社保") +@RestController +@RequestMapping("/hrm/portal/insurance/record") +@Validated +public class HrmPortalInsuranceController { + + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmInsuranceMonthEmployeeRecordService insuranceMonthEmployeeRecordService; + @Resource + private HrmInsuranceSchemeService insuranceSchemeService; + + @GetMapping("/list") + @Operation(summary = "获得我的社保记录") + @Parameter(name = "year", description = "年份", example = "2026") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getInsuranceRecordList( + @RequestParam(value = "year", required = false) Integer year) { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + List records = + insuranceMonthEmployeeRecordService.getMonthEmployeeRecordListByEmployeeIdAndYear( + employee.getId(), year); + // 拼接 VO + return success(buildInsuranceRecordRespVOList(records)); + } + + @GetMapping("/get") + @Operation(summary = "获得我的社保记录详情") + @Parameter(name = "id", description = "社保记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getInsuranceRecord(@RequestParam("id") Long id) { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + HrmInsuranceMonthEmployeeRecordDO record = insuranceMonthEmployeeRecordService.getMonthEmployeeRecord(id); + if (record == null || notEqual(record.getEmployeeId(), employee.getId())) { + throw exception(INSURANCE_MONTH_EMP_RECORD_NOT_EXISTS); + } + // 拼接 VO + return success(buildInsuranceRecordRespVO( + record, insuranceSchemeService.getScheme(record.getSchemeId()), true)); + } + + // ==================== 拼接 VO ==================== + + /** + * 构建社保记录响应列表 + * + * @param records 社保记录列表 + * @return 社保记录响应列表 + */ + private List buildInsuranceRecordRespVOList( + List records) { + Map schemeMap = insuranceSchemeService.getSchemeMap( + convertSet(records, HrmInsuranceMonthEmployeeRecordDO::getSchemeId)); + return convertList(records, record -> + buildInsuranceRecordRespVO(record, schemeMap.get(record.getSchemeId()), false)); + } + + /** + * 构建社保记录响应 + * + * @param record 社保记录 + * @param scheme 社保方案 + * @param includeProjects 是否返回社保项目 + * @return 社保记录响应 + */ + private HrmPortalInsuranceRecordRespVO buildInsuranceRecordRespVO( + HrmInsuranceMonthEmployeeRecordDO record, HrmInsuranceSchemeDO scheme, + boolean includeProjects) { + HrmPortalInsuranceRecordRespVO respVO = BeanUtils.toBean(record, HrmPortalInsuranceRecordRespVO.class); + if (scheme != null) { + respVO.setSchemeName(scheme.getName()).setSchemeType(scheme.getType()) + .setSchemeCity(AreaUtils.format(scheme.getAreaId())); + } + if (BooleanUtil.isFalse(includeProjects)) { + respVO.setProjects(null); + } + return respVO; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/vo/HrmPortalInsuranceProjectRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/vo/HrmPortalInsuranceProjectRespVO.java new file mode 100644 index 0000000..45b10c5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/vo/HrmPortalInsuranceProjectRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.insurance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工端社保项目 Response VO") +@Data +public class HrmPortalInsuranceProjectRespVO { + + @Schema(description = "社保项目编号") + private Long schemeProjectId; + + @Schema(description = "员工端社保项目类型") + private Integer type; + + @Schema(description = "社保项目名称") + private String name; + + @Schema(description = "默认缴纳金额") + private BigDecimal baseAmount; + + @Schema(description = "公司缴纳比例") + private BigDecimal corporateRate; + + @Schema(description = "个人缴纳比例") + private BigDecimal personalRate; + + @Schema(description = "公司缴纳金额") + private BigDecimal corporateAmount; + + @Schema(description = "个人缴纳金额") + private BigDecimal personalAmount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/vo/HrmPortalInsuranceRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/vo/HrmPortalInsuranceRecordRespVO.java new file mode 100644 index 0000000..c5ce6a8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/insurance/vo/HrmPortalInsuranceRecordRespVO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.insurance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端社保记录 Response VO") +@Data +public class HrmPortalInsuranceRecordRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "月度社保记录编号") + private Long monthRecordId; + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "社保方案编号") + private Long schemeId; + + @Schema(description = "社保方案名称") + private String schemeName; + + @Schema(description = "社保方案类型") + private Integer schemeType; + + @Schema(description = "参保城市") + private String schemeCity; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "个人社保金额") + private BigDecimal personalInsuranceAmount; + + @Schema(description = "个人公积金金额") + private BigDecimal personalProvidentFundAmount; + + @Schema(description = "公司社保金额") + private BigDecimal corporateInsuranceAmount; + + @Schema(description = "公司公积金金额") + private BigDecimal corporateProvidentFundAmount; + + @Schema(description = "员工端社保记录状态") + private Integer status; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "社保项目列表") + private List projects; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/HrmPortalPerformanceAssessmentController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/HrmPortalPerformanceAssessmentController.java new file mode 100644 index 0000000..0306757 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/HrmPortalPerformanceAssessmentController.java @@ -0,0 +1,354 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceAssessmentRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.performance.vo.assessment.HrmPerformanceProcessRecordRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.assessment.HrmPortalPerformanceAssessmentPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.assessment.HrmPortalPerformanceAssessmentRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.assessment.HrmPortalPerformanceTaskCountRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process.HrmPortalPerformanceAppealReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process.HrmPortalPerformanceConfirmReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process.HrmPortalPerformanceHandleStageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process.HrmPortalPerformanceProcessRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review.HrmPortalPerformanceFillQuotaReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review.HrmPortalPerformanceReviewRejectReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review.HrmPortalPerformanceScorePreviewRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review.HrmPortalPerformanceScoreReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.task.HrmPortalPerformanceTaskPageReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment.HrmPerformanceAssessmentDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment.HrmPerformanceAssessmentStageDO; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAssessmentStageStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceStageTypeEnum; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentProcessService; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentQueryService; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentReviewService; +import cn.iocoder.yudao.module.hrm.service.performance.assessment.HrmPerformanceAssessmentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端绩效考核") +@RestController +@RequestMapping("/hrm/portal/performance/assessment") +@Validated +public class HrmPortalPerformanceAssessmentController { + + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmPerformanceAssessmentService performanceAssessmentService; + @Resource + private HrmPerformanceAssessmentProcessService performanceAssessmentProcessService; + @Resource + private HrmPerformanceAssessmentReviewService performanceAssessmentReviewService; + @Resource + private HrmPerformanceAssessmentQueryService performanceAssessmentQueryService; + + @GetMapping("/page") + @Operation(summary = "获得我的绩效分页") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentPage( + @Valid HrmPortalPerformanceAssessmentPageReqVO reqVO) { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + PageResult pageResult = + performanceAssessmentService.getPortalPerformanceAssessmentPage(reqVO, employee.getId()); + // 拼接 VO + return success(buildPortalAssessmentRespVOPage(pageResult)); + } + + @GetMapping("/task-count") + @Operation(summary = "获得我的绩效任务数量") + @Parameter(name = "search", description = "员工姓名或工号", example = "张三") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getPerformanceAssessmentTaskCount( + @RequestParam(value = "search", required = false) String search) { + Long loginUserId = getLoginUserId(); + employeeService.validateEmployeeBySelf(loginUserId); + Map> taskCountMap = + performanceAssessmentProcessService.getMyTaskCountMap(loginUserId, search); + // 拼接 VO + return success(buildTaskCountRespVO(taskCountMap)); + } + + @GetMapping("/fill-quota-task-page") + @Operation(summary = "获得我的绩效指标填写任务分页") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentFillQuotaTaskPage( + @Valid HrmPortalPerformanceTaskPageReqVO reqVO) { + Long loginUserId = getLoginUserId(); + PageResult pageResult = + performanceAssessmentReviewService.getMyFillQuotaTaskPage(loginUserId, reqVO); + return success(performanceAssessmentQueryService + .getPerformanceAssessmentTaskRespVOPage(pageResult, loginUserId)); + } + + @GetMapping("/target-confirmation-task-page") + @Operation(summary = "获得我的绩效目标确认任务分页") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentTargetConfirmationTaskPage( + @Valid HrmPortalPerformanceTaskPageReqVO reqVO) { + Long loginUserId = getLoginUserId(); + PageResult pageResult = + performanceAssessmentProcessService.getMyTargetConfirmationTaskPage(loginUserId, reqVO); + return success(performanceAssessmentQueryService + .getPerformanceAssessmentTaskRespVOPage(pageResult, loginUserId)); + } + + @GetMapping("/review-task-page") + @Operation(summary = "获得我的绩效评分任务分页") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentReviewTaskPage( + @Valid HrmPortalPerformanceTaskPageReqVO reqVO) { + Long loginUserId = getLoginUserId(); + PageResult pageResult = + performanceAssessmentReviewService.getMyReviewTaskPage(loginUserId, reqVO); + return success(performanceAssessmentQueryService + .getPerformanceAssessmentTaskRespVOPage(pageResult, loginUserId)); + } + + @GetMapping("/result-audit-task-page") + @Operation(summary = "获得我的绩效结果审核任务分页") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentResultAuditTaskPage( + @Valid HrmPortalPerformanceTaskPageReqVO reqVO) { + Long loginUserId = getLoginUserId(); + PageResult pageResult = + performanceAssessmentProcessService.getMyResultAuditTaskPage(loginUserId, reqVO); + return success(performanceAssessmentQueryService + .getPerformanceAssessmentTaskRespVOPage(pageResult, loginUserId)); + } + + @GetMapping("/result-confirmation-task-page") + @Operation(summary = "获得我的绩效结果确认任务分页") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentResultConfirmationTaskPage( + @Valid HrmPortalPerformanceTaskPageReqVO reqVO) { + Long loginUserId = getLoginUserId(); + PageResult pageResult = + performanceAssessmentProcessService.getMyResultConfirmationTaskPage(loginUserId, reqVO); + return success(performanceAssessmentQueryService + .getPerformanceAssessmentTaskRespVOPage(pageResult, loginUserId)); + } + + @GetMapping("/appeal-task-page") + @Operation(summary = "获得我的绩效申诉处理任务分页") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentAppealTaskPage( + @Valid HrmPortalPerformanceTaskPageReqVO reqVO) { + Long loginUserId = getLoginUserId(); + PageResult pageResult = + performanceAssessmentProcessService.getMyAppealTaskPage(loginUserId, reqVO); + return success(performanceAssessmentQueryService + .getPerformanceAssessmentTaskRespVOPage(pageResult, loginUserId)); + } + + @GetMapping("/get") + @Operation(summary = "获得我的员工绩效考核明细") + @Parameter(name = "id", description = "员工绩效考核编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getPerformanceAssessment( + @RequestParam("id") Long id, + @RequestParam(value = "stageId", required = false) Long stageId) { + Long loginUserId = getLoginUserId(); + HrmPerformanceAssessmentDO assessment = validateReadablePerformanceAssessment(loginUserId, id, stageId); + // 拼接 VO + if (stageId != null) { + return success(performanceAssessmentQueryService + .getPerformanceAssessmentTaskRespVO(assessment, stageId, loginUserId)); + } + return success(performanceAssessmentQueryService + .getPerformanceAssessmentProcessRespVO(assessment, loginUserId)); + } + + @GetMapping("/process-record-list") + @Operation(summary = "获得我的绩效流程记录") + @Parameter(name = "id", description = "员工绩效考核编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getPerformanceAssessmentProcessRecordList( + @RequestParam("id") Long id, + @RequestParam(value = "stageId", required = false) Long stageId) { + Long loginUserId = getLoginUserId(); + HrmPerformanceAssessmentDO assessment = validateReadablePerformanceAssessment(loginUserId, id, stageId); + return success(performanceAssessmentQueryService.getPerformanceAssessmentProcessRecordList(assessment, loginUserId)); + } + + @PutMapping("/fill-quota") + @Operation(summary = "填写我的绩效指标") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult fillPerformanceAssessmentQuota( + @Valid @RequestBody HrmPortalPerformanceFillQuotaReqVO reqVO) { + Long loginUserId = getLoginUserId(); + validateOwnPerformanceAssessment(loginUserId, reqVO.getAssessmentId()); + performanceAssessmentReviewService.fillQuota(loginUserId, reqVO); + return success(true); + } + + @PutMapping("/confirm-target") + @Operation(summary = "确认我的绩效目标") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult confirmPerformanceAssessmentTarget( + @Valid @RequestBody HrmPortalPerformanceConfirmReqVO reqVO) { + performanceAssessmentProcessService.confirmTarget(getLoginUserId(), reqVO); + return success(true); + } + + @PostMapping("/score-preview") + @Operation(summary = "试算绩效评分") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult previewPerformanceAssessmentScore( + @Valid @RequestBody HrmPortalPerformanceScoreReqVO reqVO) { + return success(performanceAssessmentReviewService.previewScore(getLoginUserId(), reqVO)); + } + + @PutMapping("/score") + @Operation(summary = "提交绩效自评或他评") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult scorePerformanceAssessment( + @Valid @RequestBody HrmPortalPerformanceScoreReqVO reqVO) { + return success(performanceAssessmentReviewService.scoreAssessment(getLoginUserId(), reqVO)); + } + + @PutMapping("/reject-review-stage") + @Operation(summary = "驳回上一绩效评分阶段") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult rejectPerformanceAssessmentReviewStage( + @Valid @RequestBody HrmPortalPerformanceReviewRejectReqVO reqVO) { + performanceAssessmentReviewService.rejectReviewStage(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/handle-result-audit") + @Operation(summary = "处理绩效结果审核") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult handlePerformanceAssessmentResultAudit( + @Valid @RequestBody HrmPortalPerformanceHandleStageReqVO reqVO) { + performanceAssessmentProcessService.handleResultAudit(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/confirm-result") + @Operation(summary = "确认我的绩效结果") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult confirmPerformanceAssessmentResult( + @Valid @RequestBody HrmPortalPerformanceConfirmReqVO reqVO) { + Long loginUserId = getLoginUserId(); + validateOwnPerformanceAssessment(loginUserId, reqVO.getAssessmentId()); + performanceAssessmentProcessService.confirmResult(loginUserId, reqVO); + return success(true); + } + + @PutMapping("/submit-appeal") + @Operation(summary = "提交我的绩效申诉") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult submitPerformanceAssessmentAppeal( + @Valid @RequestBody HrmPortalPerformanceAppealReqVO reqVO) { + Long loginUserId = getLoginUserId(); + validateOwnPerformanceAssessment(loginUserId, reqVO.getAssessmentId()); + return success(performanceAssessmentProcessService.submitAppeal(loginUserId, reqVO)); + } + + @PutMapping("/handle-appeal") + @Operation(summary = "处理绩效申诉") + @PreAuthorize("@ss.hasPermission('hrm:portal:performance:action')") + public CommonResult handlePerformanceAssessmentAppeal( + @Valid @RequestBody HrmPortalPerformanceHandleStageReqVO reqVO) { + performanceAssessmentProcessService.handleAppeal(getLoginUserId(), reqVO); + return success(true); + } + + private HrmPerformanceAssessmentDO validateOwnPerformanceAssessment(Long loginUserId, Long assessmentId) { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(loginUserId); + return performanceAssessmentService.validatePerformanceAssessmentByEmployeeId(assessmentId, employee.getId()); + } + + private HrmPerformanceAssessmentDO validateReadablePerformanceAssessment( + Long loginUserId, Long assessmentId, Long stageId) { + if (stageId == null) { + return validateOwnPerformanceAssessment(loginUserId, assessmentId); + } + performanceAssessmentProcessService.validateTaskStage(loginUserId, assessmentId, stageId); + return performanceAssessmentService.getPerformanceAssessment(assessmentId); + } + + // ==================== 拼接 VO ==================== + + private PageResult buildPortalAssessmentRespVOPage( + PageResult pageResult) { + PageResult respVOPage = + performanceAssessmentQueryService.getPerformanceAssessmentRespVOPage(pageResult); + return new PageResult<>( + BeanUtils.toBean(respVOPage.getList(), HrmPortalPerformanceAssessmentRespVO.class), + respVOPage.getTotal()); + } + + private HrmPortalPerformanceTaskCountRespVO buildTaskCountRespVO( + Map> taskCountMap) { + Integer pendingStatus = HrmPerformanceAssessmentStageStatusEnum.PENDING.getStatus(); + Integer completedStatus = HrmPerformanceAssessmentStageStatusEnum.PROCESSED.getStatus(); + // 按流程类型和处理状态组装员工端各页签的待办、已办数量 + return new HrmPortalPerformanceTaskCountRespVO() + .setFillPendingCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.FILL_QUOTA.getType(), pendingStatus)) + .setFillCompletedCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.FILL_QUOTA.getType(), completedStatus)) + .setTargetPendingCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.TARGET_CONFIRM.getType(), pendingStatus)) + .setTargetCompletedCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.TARGET_CONFIRM.getType(), completedStatus)) + .setReviewPendingCount(getTaskCount(taskCountMap, HrmPerformanceStageTypeEnum.REVIEW_TYPES, pendingStatus)) + .setReviewCompletedCount(getTaskCount(taskCountMap, HrmPerformanceStageTypeEnum.REVIEW_TYPES, completedStatus)) + .setResultAuditPendingCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.RESULT_AUDIT.getType(), pendingStatus)) + .setResultAuditCompletedCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.RESULT_AUDIT.getType(), completedStatus)) + .setResultConfirmationPendingCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.RESULT_CONFIRM.getType(), pendingStatus)) + .setResultConfirmationCompletedCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.RESULT_CONFIRM.getType(), completedStatus)) + .setResultConfirmationAppealedCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.RESULT_CONFIRM.getType(), + HrmPerformanceAssessmentStageStatusEnum.APPEALED.getStatus())) + .setAppealPendingCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.APPEAL_CONFIRM.getType(), pendingStatus)) + .setAppealCompletedCount(getTaskCount( + taskCountMap, HrmPerformanceStageTypeEnum.APPEAL_CONFIRM.getType(), completedStatus)); + } + + private long getTaskCount(Map> countMap, Integer type, Integer status) { + return countMap.getOrDefault(type, Collections.emptyMap()).getOrDefault(status, 0L); + } + + @SuppressWarnings("SameParameterValue") + private long getTaskCount(Map> countMap, + Collection types, Integer status) { + long count = 0L; + for (Integer type : types) { + count += getTaskCount(countMap, type, status); + } + return count; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceAssessmentPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceAssessmentPageReqVO.java new file mode 100644 index 0000000..75e1dae --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceAssessmentPageReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.assessment; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 员工端我的绩效分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPortalPerformanceAssessmentPageReqVO extends PageParam { + + @Schema(description = "绩效计划名称", example = "2026 年第三季度绩效计划") + private String search; + + @Schema(description = "是否已归档", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否已归档不能为空") + private Boolean archived; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceAssessmentRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceAssessmentRespVO.java new file mode 100644 index 0000000..8d43217 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceAssessmentRespVO.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 员工端绩效考核 Response VO") +@Data +public class HrmPortalPerformanceAssessmentRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "绩效计划编号") + private Long planId; + + @Schema(description = "绩效计划名称") + private String name; + + @Schema(description = "考核状态") + private Integer status; + + @Schema(description = "阶段状态") + private Integer stageType; + + @Schema(description = "得分") + private BigDecimal score; + + @Schema(description = "结果等级") + private String resultLevel; + + @Schema(description = "绩效系数") + private BigDecimal coefficient; + + @Schema(description = "结果审核状态") + private Integer resultAuditStatus; + + @Schema(description = "结果审核时间") + private LocalDateTime resultAuditTime; + + @Schema(description = "结果审核意见") + private String resultAuditReason; + + @Schema(description = "申诉原因") + private String appealReason; + + @Schema(description = "申诉状态") + private Integer appealStatus; + + @Schema(description = "申诉处理时间") + private LocalDateTime appealTime; + + @Schema(description = "申诉处理意见") + private String appealComment; + + @Schema(description = "开始日期") + private LocalDateTime startTime; + + @Schema(description = "结束日期") + private LocalDateTime endTime; + + @Schema(description = "归档时间") + private LocalDateTime archiveTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceTaskCountRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceTaskCountRespVO.java new file mode 100644 index 0000000..5ffd548 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/assessment/HrmPortalPerformanceTaskCountRespVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.assessment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +@Schema(description = "管理后台 - HRM 员工端绩效任务数量 Response VO") +@Data +@Accessors(chain = true) +public class HrmPortalPerformanceTaskCountRespVO { + + @Schema(description = "待填写指标数量") + private Long fillPendingCount; + + @Schema(description = "已填写指标数量") + private Long fillCompletedCount; + + @Schema(description = "待确认目标数量") + private Long targetPendingCount; + + @Schema(description = "已确认目标数量") + private Long targetCompletedCount; + + @Schema(description = "待评分数量") + private Long reviewPendingCount; + + @Schema(description = "已评分数量") + private Long reviewCompletedCount; + + @Schema(description = "待审核结果数量") + private Long resultAuditPendingCount; + + @Schema(description = "已审核结果数量") + private Long resultAuditCompletedCount; + + @Schema(description = "待确认结果数量") + private Long resultConfirmationPendingCount; + + @Schema(description = "已确认结果数量") + private Long resultConfirmationCompletedCount; + + @Schema(description = "已申诉结果数量") + private Long resultConfirmationAppealedCount; + + @Schema(description = "待处理申诉数量") + private Long appealPendingCount; + + @Schema(description = "已处理申诉数量") + private Long appealCompletedCount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceAppealReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceAppealReqVO.java new file mode 100644 index 0000000..1f7e1c2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceAppealReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端绩效申诉 Request VO") +@Data +public class HrmPortalPerformanceAppealReqVO { + + @Schema(description = "员工绩效考核编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工绩效考核不能为空") + private Long assessmentId; + + @Schema(description = "申诉原因", requiredMode = Schema.RequiredMode.REQUIRED, + example = "实际完成结果与评分依据不一致") + @NotBlank(message = "申诉原因不能为空") + @Size(max = 500, message = "申诉原因不能超过 500 个字符") + private String appealReason; + + @Schema(description = "申诉附件地址列表", example = "[https://example.com/appeal.pdf]") + private List appealFileUrls; + + @Schema(description = "退回评分节点编号列表", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[1024, 2048]") + @NotNull(message = "退回评分节点不能为空") + private List reviewStageIds; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceConfirmReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceConfirmReqVO.java new file mode 100644 index 0000000..d1da2f7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceConfirmReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceConfirmationResultEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工端绩效确认 Request VO") +@Data +public class HrmPortalPerformanceConfirmReqVO { + + @Schema(description = "员工绩效考核编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工绩效考核不能为空") + private Long assessmentId; + + @Schema(description = "是否通过", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "确认结果不能为空") + @InEnum(value = HrmPerformanceConfirmationResultEnum.class, message = "确认结果必须是 {value}") + private Integer pass; + + @Schema(description = "说明", example = "确认考核目标") + @Size(max = 1000, message = "说明不能超过 1000 个字符") + private String comment; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceHandleStageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceHandleStageReqVO.java new file mode 100644 index 0000000..7018643 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceHandleStageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceConfirmationResultEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端绩效运行阶段处理 Request VO") +@Data +public class HrmPortalPerformanceHandleStageReqVO { + + @Schema(description = "员工绩效考核编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工绩效考核不能为空") + private Long assessmentId; + + @Schema(description = "运行阶段编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "运行阶段不能为空") + private Long stageId; + + @Schema(description = "是否通过", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "处理结果不能为空") + @InEnum(value = HrmPerformanceConfirmationResultEnum.class, message = "处理结果必须是 {value}") + private Integer pass; + + @Schema(description = "处理意见", example = "审核通过") + @Size(max = 500, message = "处理意见不能超过 500 个字符") + private String comment; + + @Schema(description = "退回评分阶段编号列表", example = "[1024, 1025]") + private List reviewStageIds; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceProcessRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceProcessRespVO.java new file mode 100644 index 0000000..d8813ab --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/process/HrmPortalPerformanceProcessRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.process; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工端绩效处理 Response VO") +@Data +public class HrmPortalPerformanceProcessRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "下一运行阶段编号") + private Long nextStageId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceFillQuotaReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceFillQuotaReqVO.java new file mode 100644 index 0000000..896f5f9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceFillQuotaReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端绩效指标填写 Request VO") +@Data +public class HrmPortalPerformanceFillQuotaReqVO { + + @Schema(description = "员工绩效考核编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工绩效考核不能为空") + private Long assessmentId; + + @Schema(description = "指标列表") + @Valid + private List quotas; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceQuotaSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceQuotaSaveReqVO.java new file mode 100644 index 0000000..3840070 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceQuotaSaveReqVO.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工绩效指标保存 Request VO") +@Data +public class HrmPortalPerformanceQuotaSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "员工绩效维度编号", example = "2048") + private Long dimensionId; + + @Schema(description = "指标名称", example = "销售目标达成率") + @Size(max = 255, message = "指标名称不能超过 255 个字符") + private String name; + + @Schema(description = "指标说明", example = "按季度销售目标计算") + @Size(max = 1000, message = "指标说明不能超过 1000 个字符") + private String description; + + @Schema(description = "标准值", example = "完成率达到 100% 得满分") + @Size(max = 1000, message = "标准值不能超过 1000 个字符") + private String standard; + + @Schema(description = "指标权重", example = "50") + private BigDecimal weight; + + @Schema(description = "分数类型", example = "1") + private Integer scoreType; + + @Schema(description = "目标值", example = "100%") + @Size(max = 1000, message = "目标值不能超过 1000 个字符") + private String targetValue; + + @Schema(description = "实际值", example = "95%") + @Size(max = 1000, message = "实际值不能超过 1000 个字符") + private String actualValue; + + @Schema(description = "自评分数", example = "90") + private BigDecimal selfScore; + + @Schema(description = "评分人得分", example = "88") + private BigDecimal reviewerScore; + + @Schema(description = "最终得分", example = "89") + private BigDecimal finalScore; + + @Schema(description = "说明", example = "目标基本达成") + @Size(max = 1000, message = "说明不能超过 1000 个字符") + private String comment; + + @Schema(description = "排序", example = "1") + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceReviewRejectReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceReviewRejectReqVO.java new file mode 100644 index 0000000..72c1321 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceReviewRejectReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 员工端绩效评分阶段驳回 Request VO") +@Data +public class HrmPortalPerformanceReviewRejectReqVO { + + @Schema(description = "员工绩效考核编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工绩效考核不能为空") + private Long assessmentId; + + @Schema(description = "评分阶段编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "当前评分阶段不能为空") + private Long reviewStageId; + + @Schema(description = "绩效评审驳回原因", requiredMode = Schema.RequiredMode.REQUIRED, + example = "评分依据不完整,请补充说明") + @NotBlank(message = "驳回原因不能为空") + @Size(max = 1000, message = "驳回原因不能超过 1000 个字符") + private String reason; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceScorePreviewRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceScorePreviewRespVO.java new file mode 100644 index 0000000..9c07933 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceScorePreviewRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 员工端绩效评分试算 Response VO") +@Data +public class HrmPortalPerformanceScorePreviewRespVO { + + @Schema(description = "阶段得分") + private BigDecimal stageScore; + + @Schema(description = "阶段结果等级") + private String stageResultLevel; + + @Schema(description = "当前累计得分") + private BigDecimal cumulativeScore; + + @Schema(description = "当前累计结果等级;全部评分阶段完成时返回") + private String cumulativeResultLevel; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceScoreReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceScoreReqVO.java new file mode 100644 index 0000000..22d19a6 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/review/HrmPortalPerformanceScoreReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.review; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端绩效评分 Request VO") +@Data +public class HrmPortalPerformanceScoreReqVO { + + @Schema(description = "员工绩效考核编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工绩效考核不能为空") + private Long assessmentId; + + @Schema(description = "评分阶段编号", example = "2048") + private Long reviewStageId; + + @Schema(description = "说明", example = "本阶段评分完成") + @Size(max = 2000, message = "说明不能超过 2000 个字符") + private String comment; + + @Schema(description = "自评说明", example = "已完成本季度目标") + @Size(max = 2000, message = "自评说明不能超过 2000 个字符") + private String selfComment; + + @Schema(description = "评分人说明", example = "目标完成情况良好") + @Size(max = 2000, message = "评分人说明不能超过 2000 个字符") + private String reviewerComment; + + @Schema(description = "指标列表") + @Valid + private List quotas; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/task/HrmPortalPerformanceTaskPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/task/HrmPortalPerformanceTaskPageReqVO.java new file mode 100644 index 0000000..abcce5c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/performance/vo/task/HrmPortalPerformanceTaskPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.performance.vo.task; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAssessmentStageStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 员工端绩效任务分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmPortalPerformanceTaskPageReqVO extends PageParam { + + @Schema(description = "绩效计划名称、员工姓名或工号", example = "张三") + private String search; + + @Schema(description = "阶段处理状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "阶段处理状态不能为空") + @InEnum(value = HrmPerformanceAssessmentStageStatusEnum.class, + message = "阶段处理状态必须是 {value}") + private Integer stageStatus; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/HrmPortalSalarySlipController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/HrmPortalSalarySlipController.java new file mode 100644 index 0000000..d433267 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/HrmPortalSalarySlipController.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.salary.vo.HrmPortalSalarySlipListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.salary.vo.HrmPortalSalarySlipRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.portal.salary.vo.HrmPortalSalarySlipUnreadSummaryRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.slip.HrmSalarySlipDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.salary.slip.HrmSalarySlipService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.hrm.enums.salary.slip.HrmSalarySlipReadStatusEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.YearMonth; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - HRM 员工端工资条") +@RestController +@RequestMapping("/hrm/portal/salary/slip") +@Validated +public class HrmPortalSalarySlipController { + + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmSalarySlipService salarySlipService; + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/unread-summary") + @Operation(summary = "获得我的未读工资条概况") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getUnreadSalarySlipSummary() { + // 1. 获得当前员工的未读工资条 + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + List unreadSalarySlips = filterList( + salarySlipService.getSalarySlipListByEmployeeId( + employee.getId(), null, null, null, null), + salarySlip -> HrmSalarySlipReadStatusEnum.UNREAD.getStatus().equals(salarySlip.getReadStatus())); + + // 2. 构建未读工资条概况 + return success(new HrmPortalSalarySlipUnreadSummaryRespVO( + (long) unreadSalarySlips.size(), + buildSalarySlipReminder(CollUtil.getFirst(unreadSalarySlips)))); + } + + @GetMapping("/list") + @Operation(summary = "获得我的工资条列表") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult> getSalarySlipList( + @Valid HrmPortalSalarySlipListReqVO reqVO) { + // 1.1 获得当前员工和月份范围 + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + YearMonth startMonth = reqVO.getStartMonth() == null ? null : YearMonth.parse(reqVO.getStartMonth()); + YearMonth endMonth = reqVO.getEndMonth() == null ? null : YearMonth.parse(reqVO.getEndMonth()); + // 1.2 查询工资条列表 + List salarySlips = salarySlipService.getSalarySlipListByEmployeeId( + employee.getId(), startMonth, endMonth, reqVO.getOrderType(), reqVO.getOrder()); + + // 2. 构建工资条响应 + return success(BeanUtils.toBean(salarySlips, HrmPortalSalarySlipRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得我的工资条详情") + @Parameter(name = "id", description = "工资条编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult getSalarySlip(@RequestParam("id") Long id) { + // 1. 获得当前员工,并校验工资条归属本人 + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + HrmSalarySlipDO salarySlip = salarySlipService.getSalarySlipByIdAndEmployeeId(id, employee.getId()); + // 2. 构建工资条响应 + return success(BeanUtils.toBean(salarySlip, HrmPortalSalarySlipRespVO.class)); + } + + @PutMapping("/read") + @Operation(summary = "标记我的工资条为已读") + @Parameter(name = "ids", description = "工资条编号列表", required = true, example = "1024,1025") + @PreAuthorize("@ss.hasPermission('hrm:portal:query')") + public CommonResult markSalarySlipRead( + @RequestParam("ids") @NotEmpty(message = "工资条编号列表不能为空") List ids) { + HrmEmployeeDO employee = employeeService.validateEmployeeBySelf(getLoginUserId()); + salarySlipService.markSalarySlipListRead(employee.getId(), ids); + return success(true); + } + + // ==================== 拼接 VO ==================== + + /** + * 构建最新未读工资条提醒 + * + * @param salarySlip 工资条 + * @return 工资条提醒 + */ + private String buildSalarySlipReminder(HrmSalarySlipDO salarySlip) { + if (salarySlip == null) { + return null; + } + String creator = salarySlip.getCreator(); + String publisherName = StrUtil.blankToDefault(creator, "HR"); + if (StrUtil.isNotBlank(creator) && StrUtil.isNumeric(creator)) { + AdminUserRespDTO user = adminUserApi.getUser(Long.valueOf(creator)); + publisherName = user == null ? "HR" : user.getNickname(); + } + return publisherName + "更新了您" + salarySlip.getMonth() + "月的工资条,前往查看"; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipListReqVO.java new file mode 100644 index 0000000..9c3ea7e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipListReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.salary.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +@Schema(description = "管理后台 - HRM 员工端工资条列表 Request VO") +@Data +public class HrmPortalSalarySlipListReqVO { + + @Schema(description = "开始月份", example = "2026-01") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "开始月份格式必须为 yyyy-MM") + private String startMonth; + + @Schema(description = "结束月份", example = "2026-12") + @Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "结束月份格式必须为 yyyy-MM") + private String endMonth; + + @Schema(description = "排序类型", example = "1") + @Range(min = 1, max = 2, message = "排序类型必须在 1 到 2 之间") + private Integer orderType; + + @Schema(description = "排序方向", example = "1") + @Range(min = 1, max = 2, message = "排序方向必须在 1 到 2 之间") + private Integer order; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipOptionRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipOptionRespVO.java new file mode 100644 index 0000000..7e88b8a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipOptionRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.salary.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端工资条项 Response VO") +@Data +public class HrmPortalSalarySlipOptionRespVO { + + @Schema(description = "员工端薪资工资条选项名称") + private String name; + + @Schema(description = "员工端薪资工资条选项类型") + private Integer type; + + @Schema(description = "编码") + private Integer code; + + @Schema(description = "值") + private BigDecimal value; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "子工资条项") + private List children; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipRespVO.java new file mode 100644 index 0000000..ad85104 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipRespVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.salary.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工端工资条 Response VO") +@Data +public class HrmPortalSalarySlipRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "工资条记录编号") + private Long sendRecordId; + + @Schema(description = "员工工资记录编号") + private Long monthEmployeeRecordId; + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "已读状态") + private Integer readStatus; + + @Schema(description = "实际工资") + private BigDecimal realPaySalary; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "选项列表") + private List options; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipUnreadSummaryRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipUnreadSummaryRespVO.java new file mode 100644 index 0000000..a3281c9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/portal/salary/vo/HrmPortalSalarySlipUnreadSummaryRespVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.portal.salary.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - HRM 员工端未读工资条概况 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class HrmPortalSalarySlipUnreadSummaryRespVO { + + @Schema(description = "未读工资条数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Long unreadCount; + + @Schema(description = "最新未读工资条提醒") + private String reminder; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitCandidateController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitCandidateController.java new file mode 100644 index 0000000..e5794db --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitCandidateController.java @@ -0,0 +1,259 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.HrmRecruitStatusCountRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateUpdateChannelReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateCleanReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateUpdateEliminateReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateEntryReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidatePageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateUpdatePostReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate.HrmRecruitCandidateUpdateStatusReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.candidate.HrmRecruitCandidateDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.config.HrmRecruitChannelDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.candidate.HrmRecruitInterviewDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostDO; +import cn.iocoder.yudao.module.hrm.enums.recruit.candidate.HrmRecruitCandidateStatusEnum; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.recruit.candidate.HrmRecruitCandidateService; +import cn.iocoder.yudao.module.hrm.service.recruit.config.HrmRecruitChannelService; +import cn.iocoder.yudao.module.hrm.service.recruit.candidate.HrmRecruitInterviewService; +import cn.iocoder.yudao.module.hrm.service.recruit.post.HrmRecruitPostService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getFirst; + +@Tag(name = "管理后台 - HRM 招聘候选人") +@RestController +@RequestMapping("/hrm/recruit/candidate") +@Validated +public class HrmRecruitCandidateController { + + @Resource + private HrmRecruitCandidateService recruitCandidateService; + @Resource + private HrmRecruitPostService recruitPostService; + @Resource + private HrmRecruitChannelService recruitChannelService; + @Resource + private HrmRecruitInterviewService recruitInterviewService; + @Resource + private HrmEmployeeService employeeService; + + @Resource + private DeptApi deptApi; + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建招聘候选人") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:create')") + public CommonResult createRecruitCandidate(@Valid @RequestBody HrmRecruitCandidateSaveReqVO createReqVO) { + return success(recruitCandidateService.createRecruitCandidate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新招聘候选人") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:update')") + public CommonResult updateRecruitCandidate(@Valid @RequestBody HrmRecruitCandidateSaveReqVO updateReqVO) { + recruitCandidateService.updateRecruitCandidate(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新招聘候选人状态") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:update')") + public CommonResult updateRecruitCandidateStatus(@Valid @RequestBody HrmRecruitCandidateUpdateStatusReqVO reqVO) { + recruitCandidateService.updateRecruitCandidateStatus(reqVO); + return success(true); + } + + @PutMapping("/update-post") + @Operation(summary = "更新招聘候选人应聘职位") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:update')") + public CommonResult updateRecruitCandidatePost(@Valid @RequestBody HrmRecruitCandidateUpdatePostReqVO reqVO) { + recruitCandidateService.updateRecruitCandidatePost(reqVO); + return success(true); + } + + @PutMapping("/update-channel") + @Operation(summary = "更新招聘候选人渠道") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:update')") + public CommonResult updateRecruitCandidateChannel( + @Valid @RequestBody HrmRecruitCandidateUpdateChannelReqVO reqVO) { + recruitCandidateService.updateRecruitCandidateChannel(reqVO); + return success(true); + } + + @PutMapping("/eliminate") + @Operation(summary = "淘汰招聘候选人") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:update')") + public CommonResult eliminateRecruitCandidate(@Valid @RequestBody HrmRecruitCandidateUpdateEliminateReqVO reqVO) { + recruitCandidateService.eliminateRecruitCandidate(reqVO); + return success(true); + } + + @PostMapping("/convert-employee") + @Operation(summary = "候选人转员工档案") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:update')") + public CommonResult convertRecruitCandidateToEmployee( + @Valid @RequestBody HrmRecruitCandidateEntryReqVO reqVO) { + return success(recruitCandidateService.convertRecruitCandidateToEmployee(reqVO)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除招聘候选人") + @Parameter(name = "id", description = "候选人编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:delete')") + public CommonResult deleteRecruitCandidate(@RequestParam("id") Long id) { + recruitCandidateService.deleteRecruitCandidate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得招聘候选人") + @Parameter(name = "id", description = "候选人编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:query')") + public CommonResult getRecruitCandidate(@RequestParam("id") Long id) { + return success(buildRecruitCandidateRespVO(recruitCandidateService.getRecruitCandidate(id))); + } + + @GetMapping("/page") + @Operation(summary = "获得招聘候选人分页") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:query')") + public CommonResult> getRecruitCandidatePage( + @Validated HrmRecruitCandidatePageReqVO pageReqVO) { + PageResult pageResult = recruitCandidateService.getRecruitCandidatePage(pageReqVO); + return success(new PageResult<>(buildRecruitCandidateRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/clean-ids") + @Operation(summary = "获得待清理候选人编号") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:delete')") + public CommonResult> getCleanRecruitCandidateIdList(@Valid HrmRecruitCandidateCleanReqVO reqVO) { + List list = recruitCandidateService.getRecruitCandidateList( + reqVO.getStatuses(), LocalDateTime.now().minusDays(reqVO.getDays())); + return success(convertList(list, HrmRecruitCandidateDO::getId)); + } + + @GetMapping("/status-count") + @Operation(summary = "获得候选人状态数量") + @PreAuthorize("@ss.hasPermission('hrm:recruit:candidate:query')") + public CommonResult> getRecruitCandidateStatusCount( + @Validated HrmRecruitCandidatePageReqVO pageReqVO) { + Map countMap = recruitCandidateService.getRecruitCandidateStatusCount(pageReqVO); + return success(convertList(Arrays.asList(HrmRecruitCandidateStatusEnum.values()), + status -> new HrmRecruitStatusCountRespVO(status.getStatus(), + countMap.getOrDefault(status.getStatus(), 0L)))); + } + + // ==================== 拼接 VO ==================== + + private HrmRecruitCandidateRespVO buildRecruitCandidateRespVO(HrmRecruitCandidateDO recruitCandidate) { + if (recruitCandidate == null) { + return null; + } + return getFirst(buildRecruitCandidateRespVOList(Collections.singletonList(recruitCandidate))); + } + + private List buildRecruitCandidateRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 1.1 招聘职位、部门和招聘渠道信息 + Map recruitPostMap = recruitPostService.getRecruitPostMap( + convertSet(list, HrmRecruitCandidateDO::getPostId)); + Map deptMap = deptApi.getDeptMap( + convertSet(recruitPostMap.values(), HrmRecruitPostDO::getDeptId)); + Map recruitChannelMap = recruitChannelService.getRecruitChannelMap( + convertSet(list, HrmRecruitCandidateDO::getChannelId)); + // 1.2 当前面试和员工信息 + Set candidateIds = convertSet(list, HrmRecruitCandidateDO::getId); + Map recruitInterviewMap = recruitInterviewService + .getLatestRecruitInterviewMapByCandidateIds(candidateIds); + Set employeeIds = convertSet(recruitPostMap.values(), HrmRecruitPostDO::getOwnerEmployeeId); + employeeIds.addAll(convertSet(recruitInterviewMap.values(), HrmRecruitInterviewDO::getInterviewEmployeeId)); + employeeIds.addAll(convertSetByFlatMap(recruitInterviewMap.values(), + HrmRecruitInterviewDO::getOtherInterviewEmployeeIds, + Collection::stream)); + Map employeeMap = employeeService.getEmployeeMap(employeeIds); + Map candidateEmployeeMap = employeeService.getEmployeeMapByCandidateIds(candidateIds); + // 1.3 创建人信息 + Map userMap = adminUserApi.getUserMap( + convertSet(list, candidate -> NumberUtil.parseLong(candidate.getCreator()))); + + // 2. 拼接响应 + return BeanUtils.toBean(list, HrmRecruitCandidateRespVO.class, vo -> { + // 2.1 拼接招聘职位、部门和招聘渠道信息 + MapUtils.findAndThen(recruitPostMap, vo.getPostId(), post -> { + vo.setPostName(post.getPostName()).setPostStatus(post.getStatus()) + .setDeptId(post.getDeptId()).setOwnerEmployeeId(post.getOwnerEmployeeId()); + MapUtils.findAndThen(deptMap, post.getDeptId(), dept -> vo.setDeptName(dept.getName())); + MapUtils.findAndThen(employeeMap, post.getOwnerEmployeeId(), + employee -> vo.setOwnerEmployeeName(employee.getName())); + }); + MapUtils.findAndThen(recruitChannelMap, vo.getChannelId(), + channel -> vo.setChannelName(channel.getName())); + // 2.2 拼接面试信息 + if (HrmRecruitCandidateStatusEnum.INTERVIEW_RELATED_STATUSES.contains(vo.getStatus())) { + MapUtils.findAndThen(recruitInterviewMap, vo.getId(), interview -> { + vo.setInterviewId(interview.getId()).setInterviewType(interview.getType()) + .setInterviewEmployeeId(interview.getInterviewEmployeeId()) + .setOtherInterviewEmployeeIds(interview.getOtherInterviewEmployeeIds()) + .setInterviewTime(interview.getInterviewTime()).setInterviewAddress(interview.getAddress()) + .setInterviewResult(interview.getResult()) + .setOtherInterviewEmployeeNames(convertList( + convertList(interview.getOtherInterviewEmployeeIds(), employeeMap::get), + HrmEmployeeDO::getName)); + MapUtils.findAndThen(employeeMap, interview.getInterviewEmployeeId(), + employee -> vo.setInterviewEmployeeName(employee.getName())); + }); + } else { + vo.setStageNumber(0); + } + // 2.3 拼接员工和创建人信息 + MapUtils.findAndThen(candidateEmployeeMap, vo.getId(), employee -> vo.setEmployeeId(employee.getId())); + MapUtils.findAndThen(userMap, NumberUtil.parseLong(vo.getCreator()), + creator -> vo.setCreatorName(creator.getNickname())); + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitChannelController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitChannelController.java new file mode 100644 index 0000000..d45f827 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitChannelController.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel.HrmRecruitChannelDeleteReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel.HrmRecruitChannelPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel.HrmRecruitChannelRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel.HrmRecruitChannelSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel.HrmRecruitChannelStatusReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.config.HrmRecruitChannelDO; +import cn.iocoder.yudao.module.hrm.service.recruit.config.HrmRecruitChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - HRM 招聘渠道") +@RestController +@RequestMapping("/hrm/recruit/channel") +@Validated +public class HrmRecruitChannelController { + + @Resource + private HrmRecruitChannelService recruitChannelService; + + @PostMapping("/create") + @Operation(summary = "创建招聘渠道") + @PreAuthorize("@ss.hasPermission('hrm:recruit:channel:create')") + public CommonResult createRecruitChannel(@Valid @RequestBody HrmRecruitChannelSaveReqVO createReqVO) { + return success(recruitChannelService.createRecruitChannel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新招聘渠道") + @PreAuthorize("@ss.hasPermission('hrm:recruit:channel:update')") + public CommonResult updateRecruitChannel(@Valid @RequestBody HrmRecruitChannelSaveReqVO updateReqVO) { + recruitChannelService.updateRecruitChannel(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新招聘渠道状态") + @PreAuthorize("@ss.hasPermission('hrm:recruit:channel:update')") + public CommonResult updateRecruitChannelStatus( + @Valid @RequestBody HrmRecruitChannelStatusReqVO statusReqVO) { + recruitChannelService.updateRecruitChannelStatus(statusReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除招聘渠道") + @PreAuthorize("@ss.hasPermission('hrm:recruit:channel:delete')") + public CommonResult deleteRecruitChannel(@Valid @RequestBody HrmRecruitChannelDeleteReqVO deleteReqVO) { + recruitChannelService.deleteRecruitChannel(deleteReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得招聘渠道") + @Parameter(name = "id", description = "招聘渠道编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:recruit:channel:query')") + public CommonResult getRecruitChannel(@RequestParam("id") Long id) { + HrmRecruitChannelDO recruitChannel = recruitChannelService.getRecruitChannel(id); + return success(BeanUtils.toBean(recruitChannel, HrmRecruitChannelRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得招聘渠道分页") + @PreAuthorize("@ss.hasPermission('hrm:recruit:channel:query')") + public CommonResult> getRecruitChannelPage(@Validated HrmRecruitChannelPageReqVO pageReqVO) { + PageResult pageResult = recruitChannelService.getRecruitChannelPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, HrmRecruitChannelRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得招聘渠道精简列表", description = "用于招聘职位、候选人等表单的下拉选项") + public CommonResult> getRecruitChannelSimpleList() { + return success(convertList(recruitChannelService.getRecruitChannelSimpleList(), channel -> + new HrmRecruitChannelRespVO().setId(channel.getId()).setName(channel.getName()).setStatus(channel.getStatus()))); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitConfigController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitConfigController.java new file mode 100644 index 0000000..f50147d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitConfigController.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.config.HrmRecruitEliminateReasonSaveReqVO; +import cn.iocoder.yudao.module.hrm.service.recruit.config.HrmRecruitConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 招聘设置") +@RestController +@RequestMapping("/hrm/recruit/config") +@Validated +public class HrmRecruitConfigController { + + @Resource + private HrmRecruitConfigService recruitConfigService; + + @PostMapping("/eliminate-reason/save") + @Operation(summary = "保存招聘淘汰原因") + @PreAuthorize("@ss.hasPermission('hrm:recruit:config:update')") + public CommonResult saveRecruitEliminateReason( + @Valid @RequestBody HrmRecruitEliminateReasonSaveReqVO saveReqVO) { + recruitConfigService.saveRecruitEliminateReason(saveReqVO); + return success(true); + } + + @GetMapping("/eliminate-reason/list") + @Operation(summary = "获得招聘淘汰原因列表") + @PreAuthorize("@ss.hasPermission('hrm:recruit:config:query') " + + "or @ss.hasPermission('hrm:recruit:candidate:update')") + public CommonResult> getRecruitEliminateReasonList() { + return success(recruitConfigService.getRecruitEliminateReasonList()); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitInterviewController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitInterviewController.java new file mode 100644 index 0000000..17257ac --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitInterviewController.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.interview.HrmRecruitInterviewRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.interview.HrmRecruitInterviewResultReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.interview.HrmRecruitInterviewSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.candidate.HrmRecruitInterviewDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.recruit.candidate.HrmRecruitInterviewService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getFirst; + +@Tag(name = "管理后台 - HRM 招聘面试") +@RestController +@RequestMapping("/hrm/recruit/interview") +@Validated +public class HrmRecruitInterviewController { + + @Resource + private HrmRecruitInterviewService recruitInterviewService; + @Resource + private HrmEmployeeService employeeService; + + @PostMapping("/create") + @Operation(summary = "创建招聘面试") + @PreAuthorize("@ss.hasPermission('hrm:recruit:interview:create')") + public CommonResult createRecruitInterview(@Valid @RequestBody HrmRecruitInterviewSaveReqVO createReqVO) { + return success(recruitInterviewService.createRecruitInterview(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新招聘面试") + @PreAuthorize("@ss.hasPermission('hrm:recruit:interview:update')") + public CommonResult updateRecruitInterview(@Valid @RequestBody HrmRecruitInterviewSaveReqVO updateReqVO) { + recruitInterviewService.updateRecruitInterview(updateReqVO); + return success(true); + } + + @PutMapping("/update-result") + @Operation(summary = "更新招聘面试结果") + @PreAuthorize("@ss.hasPermission('hrm:recruit:interview:update')") + public CommonResult updateRecruitInterviewResult(@Valid @RequestBody HrmRecruitInterviewResultReqVO reqVO) { + recruitInterviewService.updateRecruitInterviewResult(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除招聘面试") + @Parameter(name = "id", description = "面试编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:recruit:interview:delete')") + public CommonResult deleteRecruitInterview(@RequestParam("id") Long id) { + recruitInterviewService.deleteRecruitInterview(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得招聘面试") + @Parameter(name = "id", description = "面试编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:recruit:interview:query')") + public CommonResult getRecruitInterview(@RequestParam("id") Long id) { + return success(buildRecruitInterviewRespVO(recruitInterviewService.getRecruitInterview(id))); + } + + @GetMapping("/list-by-candidate") + @Operation(summary = "获得候选人的招聘面试列表") + @Parameter(name = "candidateId", description = "候选人编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:recruit:interview:query')") + public CommonResult> getRecruitInterviewListByCandidate( + @RequestParam("candidateId") Long candidateId) { + return success(buildRecruitInterviewRespVOList( + recruitInterviewService.getRecruitInterviewListByCandidateId(candidateId))); + } + + // ==================== 拼接 VO ==================== + + private HrmRecruitInterviewRespVO buildRecruitInterviewRespVO(HrmRecruitInterviewDO recruitInterview) { + if (recruitInterview == null) { + return null; + } + return getFirst(buildRecruitInterviewRespVOList(Collections.singletonList(recruitInterview))); + } + + private List buildRecruitInterviewRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 1. 获得面试官信息 + Set employeeIds = convertSet(list, HrmRecruitInterviewDO::getInterviewEmployeeId); + employeeIds.addAll(convertSetByFlatMap(list, HrmRecruitInterviewDO::getOtherInterviewEmployeeIds, + Collection::stream)); + Map employeeMap = employeeService.getEmployeeMap(employeeIds); + + // 2. 拼接响应 + return BeanUtils.toBean(list, HrmRecruitInterviewRespVO.class, vo -> { + MapUtils.findAndThen(employeeMap, vo.getInterviewEmployeeId(), + employee -> vo.setInterviewEmployeeName(employee.getName())); + vo.setOtherInterviewEmployeeNames(convertList( + convertList(vo.getOtherInterviewEmployeeIds(), employeeMap::get), + HrmEmployeeDO::getName)); + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitPostController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitPostController.java new file mode 100644 index 0000000..2251e2a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitPostController.java @@ -0,0 +1,181 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.HrmRecruitStatusCountRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post.HrmRecruitPostPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post.HrmRecruitPostRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post.HrmRecruitPostSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post.HrmRecruitPostStatusReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostTypeDO; +import cn.iocoder.yudao.module.hrm.enums.recruit.post.HrmRecruitPostStatusEnum; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.recruit.candidate.HrmRecruitCandidateService; +import cn.iocoder.yudao.module.hrm.service.recruit.post.HrmRecruitPostService; +import cn.iocoder.yudao.module.hrm.service.recruit.post.HrmRecruitPostTypeService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getFirst; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 招聘职位") +@RestController +@RequestMapping("/hrm/recruit/post") +@Validated +public class HrmRecruitPostController { + + @Resource + private HrmRecruitPostService recruitPostService; + @Resource + private HrmRecruitCandidateService recruitCandidateService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmRecruitPostTypeService recruitPostTypeService; + + @Resource + private DeptApi deptApi; + + @PostMapping("/create") + @Operation(summary = "创建招聘职位") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:create')") + public CommonResult createRecruitPost(@Valid @RequestBody HrmRecruitPostSaveReqVO createReqVO) { + return success(recruitPostService.createRecruitPost(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新招聘职位") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:update')") + public CommonResult updateRecruitPost(@Valid @RequestBody HrmRecruitPostSaveReqVO updateReqVO) { + recruitPostService.updateRecruitPost(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新招聘职位状态") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:update')") + public CommonResult updateRecruitPostStatus(@Valid @RequestBody HrmRecruitPostStatusReqVO reqVO) { + recruitPostService.updateRecruitPostStatus(reqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得招聘职位") + @Parameter(name = "id", description = "招聘职位编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:query')") + public CommonResult getRecruitPost(@RequestParam("id") Long id) { + return success(buildRecruitPostRespVO(recruitPostService.getRecruitPost(id))); + } + + @GetMapping("/page") + @Operation(summary = "获得招聘职位分页") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:query')") + public CommonResult> getRecruitPostPage( + @Validated HrmRecruitPostPageReqVO pageReqVO) { + PageResult pageResult = recruitPostService.getRecruitPostPage(pageReqVO); + return success(new PageResult<>(buildRecruitPostRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得招聘职位精简列表") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:query')") + public CommonResult> getRecruitPostSimpleList() { + return success(convertList(recruitPostService.getRecruitPostSimpleList(), post -> + new HrmRecruitPostRespVO().setId(post.getId()).setPostName(post.getPostName()) + .setStatus(post.getStatus()))); + } + + @GetMapping("/status-count") + @Operation(summary = "获得招聘职位状态数量") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:query')") + public CommonResult> getRecruitPostStatusCount( + @Validated HrmRecruitPostPageReqVO pageReqVO) { + Map countMap = recruitPostService.getRecruitPostStatusCount(pageReqVO); + return success(convertList(Arrays.asList(HrmRecruitPostStatusEnum.values()), + status -> new HrmRecruitStatusCountRespVO(status.getStatus(), + countMap.getOrDefault(status.getStatus(), 0L)))); + } + + // ==================== 拼接 VO ==================== + + private HrmRecruitPostRespVO buildRecruitPostRespVO(HrmRecruitPostDO recruitPost) { + if (recruitPost == null) { + return null; + } + return getFirst(buildRecruitPostRespVOList(Collections.singletonList(recruitPost))); + } + + private List buildRecruitPostRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 1.1 员工信息 + Set employeeIds = convertSet(list, HrmRecruitPostDO::getOwnerEmployeeId); + employeeIds.addAll(convertSetByFlatMap(list, HrmRecruitPostDO::getInterviewEmployeeIds, + Collection::stream)); + Map employeeMap = employeeService.getEmployeeMap(employeeIds); + // 1.2 部门信息 + Map deptMap = deptApi.getDeptMap(convertSet(list, HrmRecruitPostDO::getDeptId)); + // 1.3 职位类型信息 + Map postTypeMap = recruitPostTypeService.getRecruitPostTypeMap( + convertSet(list, HrmRecruitPostDO::getPostTypeId)); + // 1.4 已入职人数 + Map joinedCountMap = recruitCandidateService.getJoinedCandidateCountMap( + convertSet(list, HrmRecruitPostDO::getId)); + // 2. 拼接响应 + return BeanUtils.toBean(list, HrmRecruitPostRespVO.class, vo -> { + vo.setAreaName(AreaUtils.format(vo.getAreaId())); + MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName())); + MapUtils.findAndThen(employeeMap, vo.getOwnerEmployeeId(), + employee -> vo.setOwnerEmployeeName(employee.getName())); + vo.setInterviewEmployeeNames(convertList(vo.getInterviewEmployeeIds(), + employeeId -> employeeMap.containsKey(employeeId) + ? employeeMap.get(employeeId).getName() : null)); + MapUtils.findAndThen(postTypeMap, vo.getPostTypeId(), + postType -> vo.setPostTypeName(postType.getName())); + fillRecruitPostProgress(vo, joinedCountMap.getOrDefault(vo.getId(), 0L)); + }); + } + + private void fillRecruitPostProgress(HrmRecruitPostRespVO recruitPost, Long hasEntryNum) { + recruitPost.setHasEntryNum(hasEntryNum); + if (recruitPost.getRecruitNum() != null && recruitPost.getRecruitNum() != 0) { + recruitPost.setRecruitSchedule(BigDecimal.valueOf(hasEntryNum).multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(recruitPost.getRecruitNum()), 2, RoundingMode.HALF_UP)); + } + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitPostTypeController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitPostTypeController.java new file mode 100644 index 0000000..bd1ad14 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/HrmRecruitPostTypeController.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post.type.HrmRecruitPostTypeRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostTypeDO; +import cn.iocoder.yudao.module.hrm.service.recruit.post.HrmRecruitPostTypeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 招聘职位类型") +@RestController +@RequestMapping("/hrm/recruit/post-type") +@Validated +public class HrmRecruitPostTypeController { + + @Resource + private HrmRecruitPostTypeService recruitPostTypeService; + + @GetMapping("/list") + @Operation(summary = "获得招聘职位类型列表") + @Parameter(name = "status", description = "状态", example = "0") + @PreAuthorize("@ss.hasPermission('hrm:recruit:post:query')") + public CommonResult> getRecruitPostTypeList(@RequestParam(value = "status", required = false) Integer status) { + List postTypes = recruitPostTypeService.getRecruitPostTypeList(status); + return success(BeanUtils.toBean(postTypes, HrmRecruitPostTypeRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/HrmRecruitStatusCountRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/HrmRecruitStatusCountRespVO.java new file mode 100644 index 0000000..52f714d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/HrmRecruitStatusCountRespVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - HRM 招聘状态数量 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class HrmRecruitStatusCountRespVO { + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "数量", example = "12") + private Long count; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateCleanReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateCleanReqVO.java new file mode 100644 index 0000000..4ef3fcd --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateCleanReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.recruit.candidate.HrmRecruitCandidateStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 招聘候选人清理 Request VO") +@Data +public class HrmRecruitCandidateCleanReqVO { + + @Schema(description = "候选人状态数组", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[1, 2, 3, 4]") + @NotEmpty(message = "候选人状态不能为空") + private List<@NotNull(message = "候选人状态不能为空") + @InEnum(value = HrmRecruitCandidateStatusEnum.class, message = "候选人状态必须是 {value}") Integer> statuses; + + @Schema(description = "状态持续天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + @NotNull(message = "状态持续天数不能为空") + @Positive(message = "状态持续天数必须大于 0") + private Integer days; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateEntryReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateEntryReqVO.java new file mode 100644 index 0000000..dc2a7cc --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateEntryReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeSaveReqVO; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 招聘候选人转员工 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmRecruitCandidateEntryReqVO extends HrmEmployeeSaveReqVO { + + @AssertTrue(message = "候选人编号不能为空") + @JsonIgnore + public boolean isCandidateIdValid() { + return getCandidateId() != null; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidatePageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidatePageReqVO.java new file mode 100644 index 0000000..f84f00b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidatePageReqVO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.recruit.candidate.HrmRecruitCandidateStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 招聘候选人分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmRecruitCandidatePageReqVO extends PageParam { + + @Schema(description = "姓名、手机号码或邮箱,模糊匹配", example = "张三") + private String search; + + @Schema(description = "职位编号", example = "1024") + private Long postId; + + @Schema(description = "招聘负责人员工编号", example = "1") + private Long ownerEmployeeId; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "最小年龄", example = "20") + private Integer minAge; + + @Schema(description = "最大年龄", example = "35") + private Integer maxAge; + + @Schema(description = "最小工作年限", example = "1") + private Integer minWorkTime; + + @Schema(description = "最大工作年限", example = "5") + private Integer maxWorkTime; + + @Schema(description = "学历", example = "5") + private Integer education; + + @Schema(description = "毕业院校,模糊匹配", example = "浙江大学") + private String graduateSchool; + + @Schema(description = "最近工作单位,模糊匹配", example = "某科技公司") + private String latestWorkPlace; + + @Schema(description = "渠道编号", example = "1") + private Long channelId; + + @Schema(description = "面试官员工编号", example = "1") + private Long interviewEmployeeId; + + @Schema(description = "面试时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] interviewTime; + + @Schema(description = "创建人用户编号", example = "1") + private Long creator; + + @Schema(description = "候选人状态", example = "1") + @InEnum(value = HrmRecruitCandidateStatusEnum.class, message = "候选人状态必须是 {value}") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateRespVO.java new file mode 100644 index 0000000..16f22fb --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateRespVO.java @@ -0,0 +1,135 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 招聘候选人 Response VO") +@Data +@Accessors(chain = true) +public class HrmRecruitCandidateRespVO { + + @Schema(description = "候选人编号", example = "1024") + private Long id; + + @Schema(description = "候选人姓名", example = "张三") + private String name; + + @Schema(description = "手机号码", example = "15601691399") + private String mobile; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "年龄", example = "28") + private Integer age; + + @Schema(description = "邮箱", example = "candidate@example.com") + private String email; + + @Schema(description = "应聘职位编号", example = "1024") + private Long postId; + + @Schema(description = "应聘职位名称", example = "Java 开发工程师") + private String postName; + + @Schema(description = "应聘职位状态", example = "1") + private Integer postStatus; + + @Schema(description = "用人部门编号", example = "100") + private Long deptId; + + @Schema(description = "用人部门名称", example = "研发部") + private String deptName; + + @Schema(description = "招聘负责人员工编号", example = "1") + private Long ownerEmployeeId; + + @Schema(description = "招聘负责人姓名", example = "李四") + private String ownerEmployeeName; + + @Schema(description = "面试轮次", example = "1") + private Integer stageNumber; + + @Schema(description = "工作年限", example = "5") + private Integer workTime; + + @Schema(description = "学历", example = "5") + private Integer education; + + @Schema(description = "毕业院校", example = "浙江大学") + private String graduateSchool; + + @Schema(description = "最近工作单位", example = "某科技公司") + private String latestWorkPlace; + + @Schema(description = "招聘渠道编号", example = "1") + private Long channelId; + + @Schema(description = "招聘渠道名称", example = "BOSS 直聘") + private String channelName; + + @Schema(description = "备注", example = "沟通意向强") + private String remark; + + @Schema(description = "候选人状态", example = "1") + private Integer status; + + @Schema(description = "淘汰原因", example = "技能不匹配") + private String eliminate; + + @Schema(description = "状态更新时间") + private LocalDateTime statusUpdateTime; + + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + @Schema(description = "简历附件地址数组", example = "[\"https://example.com/resume.pdf\"]") + private List resumeUrls; + + @Schema(description = "当前面试编号", example = "1024") + private Long interviewId; + + @Schema(description = "面试方式", example = "3") + private Integer interviewType; + + @Schema(description = "主面试官员工编号", example = "1") + private Long interviewEmployeeId; + + @Schema(description = "主面试官姓名", example = "王五") + private String interviewEmployeeName; + + @Schema(description = "其他面试官员工编号数组", example = "[2, 3]") + private List otherInterviewEmployeeIds; + + @Schema(description = "其他面试官姓名数组", example = "[\"赵六\", \"钱七\"]") + private List otherInterviewEmployeeNames; + + @Schema(description = "面试时间") + private LocalDateTime interviewTime; + + @Schema(description = "面试地址", example = "上海市浦东新区") + private String interviewAddress; + + @Schema(description = "面试结果", example = "1") + private Integer interviewResult; + + @Schema(description = "转入的员工编号", example = "100") + private Long employeeId; + + @Schema(description = "创建人", example = "1") + private String creator; + + @Schema(description = "创建人名称", example = "管理员") + private String creatorName; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateSaveReqVO.java new file mode 100644 index 0000000..796f19c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateSaveReqVO.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 招聘候选人新增/修改 Request VO") +@Data +public class HrmRecruitCandidateSaveReqVO { + + @Schema(description = "候选人编号", example = "1024") + private Long id; + + @Schema(description = "候选人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotBlank(message = "候选人姓名不能为空") + @Size(max = 255, message = "候选人姓名长度不能超过 255 个字符") + @DiffLogField(name = "候选人姓名") + private String name; + + @Schema(description = "手机号码", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691399") + @NotBlank(message = "手机号码不能为空") + @Size(max = 18, message = "手机号码长度不能超过 18 个字符") + @Pattern(regexp = "^(\\+?0?\\d{2,4}-?)?\\d{6,11}$", message = "手机号码格式不正确") + @DiffLogField(name = "手机号码") + private String mobile; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "性别不能为空") + @DiffLogField(name = "性别") + private Integer sex; + + @Schema(description = "年龄", example = "28") + @Min(value = 0, message = "年龄不能小于 0") + @Max(value = 99, message = "年龄不能大于 99") + @DiffLogField(name = "年龄") + private Integer age; + + @Schema(description = "邮箱", example = "candidate@example.com") + @Email(message = "邮箱格式不正确") + @Size(max = 255, message = "邮箱长度不能超过 255 个字符") + @DiffLogField(name = "邮箱") + private String email; + + @Schema(description = "应聘职位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应聘职位不能为空") + @DiffLogField(name = "应聘职位") + private Long postId; + + @Schema(description = "工作年限", example = "5") + @Min(value = 0, message = "工作年限不能小于 0") + @Max(value = 60, message = "工作年限不能大于 60") + @DiffLogField(name = "工作年限") + private Integer workTime; + + @Schema(description = "学历", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "学历不能为空") + @DiffLogField(name = "学历") + private Integer education; + + @Schema(description = "毕业院校", example = "浙江大学") + @Size(max = 255, message = "毕业院校长度不能超过 255 个字符") + @DiffLogField(name = "毕业院校") + private String graduateSchool; + + @Schema(description = "最近工作单位", example = "某科技公司") + @Size(max = 255, message = "最近工作单位长度不能超过 255 个字符") + @DiffLogField(name = "最近工作单位") + private String latestWorkPlace; + + @Schema(description = "招聘渠道编号", example = "1") + @DiffLogField(name = "招聘渠道") + private Long channelId; + + @Schema(description = "备注", example = "沟通意向强") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + @DiffLogField(name = "备注") + private String remark; + + @Schema(description = "简历附件地址数组", example = "[\"https://example.com/resume.pdf\"]") + @Size(max = 5, message = "简历附件不能超过 5 个") + @DiffLogField(name = "简历附件") + private List<@Size(max = 512, message = "简历附件地址长度不能超过 512 个字符") String> resumeUrls; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateChannelReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateChannelReqVO.java new file mode 100644 index 0000000..9b1443c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateChannelReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘候选人渠道修改 Request VO") +@Data +public class HrmRecruitCandidateUpdateChannelReqVO { + + @Schema(description = "候选人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "候选人编号不能为空") + private Long id; + + @Schema(description = "招聘渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "招聘渠道编号不能为空") + private Long channelId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateEliminateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateEliminateReqVO.java new file mode 100644 index 0000000..e1da481 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateEliminateReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘候选人淘汰 Request VO") +@Data +public class HrmRecruitCandidateUpdateEliminateReqVO { + + @Schema(description = "候选人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "候选人编号不能为空") + private Long id; + + @Schema(description = "淘汰原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "技能不匹配") + @NotBlank(message = "淘汰原因不能为空") + @Size(max = 255, message = "淘汰原因长度不能超过 255 个字符") + private String eliminate; + + @Schema(description = "备注", example = "二面未通过") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdatePostReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdatePostReqVO.java new file mode 100644 index 0000000..d756236 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdatePostReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘候选人职位修改 Request VO") +@Data +public class HrmRecruitCandidateUpdatePostReqVO { + + @Schema(description = "候选人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "候选人编号不能为空") + private Long id; + + @Schema(description = "职位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "职位编号不能为空") + private Long postId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateStatusReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateStatusReqVO.java new file mode 100644 index 0000000..850b84e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/candidate/HrmRecruitCandidateUpdateStatusReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.candidate; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.recruit.candidate.HrmRecruitCandidateStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘候选人状态修改 Request VO") +@Data +public class HrmRecruitCandidateUpdateStatusReqVO { + + @Schema(description = "候选人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "候选人编号不能为空") + private Long id; + + @Schema(description = "候选人状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "候选人状态不能为空") + @InEnum(value = HrmRecruitCandidateStatusEnum.class, message = "候选人状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelDeleteReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelDeleteReqVO.java new file mode 100644 index 0000000..888380a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelDeleteReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘渠道删除 Request VO") +@Data +public class HrmRecruitChannelDeleteReqVO { + + @Schema(description = "待删除招聘渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "待删除招聘渠道编号不能为空") + private Long id; + + @Schema(description = "承接招聘渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1025") + @NotNull(message = "承接招聘渠道编号不能为空") + private Long transferChannelId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelPageReqVO.java new file mode 100644 index 0000000..a6e1f8d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelPageReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 招聘渠道分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmRecruitChannelPageReqVO extends PageParam { + + @Schema(description = "渠道名称,模糊匹配", example = "BOSS") + private String name; + + @Schema(description = "状态", example = "0") + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelRespVO.java new file mode 100644 index 0000000..ccbd668 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 招聘渠道 Response VO") +@Data +public class HrmRecruitChannelRespVO { + + @Schema(description = "招聘渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "是否系统内置", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean systemFlag; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "渠道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "BOSS 直聘") + private String name; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer sort; + + @Schema(description = "备注", example = "常用渠道") + private String remark; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelSaveReqVO.java new file mode 100644 index 0000000..baadd03 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelSaveReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘渠道新增/修改 Request VO") +@Data +public class HrmRecruitChannelSaveReqVO { + + @Schema(description = "招聘渠道编号", example = "1024") + private Long id; + + @Schema(description = "渠道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "BOSS 直聘") + @NotBlank(message = "渠道名称不能为空") + @Size(max = 255, message = "渠道名称不能超过 255 个字符") + @DiffLogField(name = "渠道名称") + private String name; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "显示顺序不能为空") + @Min(value = 0, message = "显示顺序不能小于 0") + @DiffLogField(name = "显示顺序") + private Integer sort; + + @Schema(description = "备注", example = "常用渠道") + @Size(max = 500, message = "备注不能超过 500 个字符") + @DiffLogField(name = "备注") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelStatusReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelStatusReqVO.java new file mode 100644 index 0000000..e09122c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/channel/HrmRecruitChannelStatusReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.channel; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘渠道状态修改 Request VO") +@Data +public class HrmRecruitChannelStatusReqVO { + + @Schema(description = "招聘渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "招聘渠道编号不能为空") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/config/HrmRecruitEliminateReasonSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/config/HrmRecruitEliminateReasonSaveReqVO.java new file mode 100644 index 0000000..dfc0dc5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/config/HrmRecruitEliminateReasonSaveReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.config; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 招聘淘汰原因保存 Request VO") +@Data +public class HrmRecruitEliminateReasonSaveReqVO { + + @Schema(description = "淘汰原因列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "淘汰原因不能为空") + @DiffLogField(name = "淘汰原因") + private List<@NotBlank(message = "淘汰原因不能为空") String> reasons; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewRespVO.java new file mode 100644 index 0000000..7cbb6c3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewRespVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.interview; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 招聘面试 Response VO") +@Data +public class HrmRecruitInterviewRespVO { + + @Schema(description = "面试编号", example = "1024") + private Long id; + + @Schema(description = "候选人编号", example = "1024") + private Long candidateId; + + @Schema(description = "面试方式", example = "1") + private Integer type; + + @Schema(description = "面试轮次", example = "1") + private Integer stageNumber; + + @Schema(description = "主面试官员工编号", example = "1") + private Long interviewEmployeeId; + + @Schema(description = "主面试官姓名", example = "张三") + private String interviewEmployeeName; + + @Schema(description = "其他面试官员工编号数组", example = "[2, 3]") + private List otherInterviewEmployeeIds; + + @Schema(description = "其他面试官姓名数组", example = "[\"李四\", \"王五\"]") + private List otherInterviewEmployeeNames; + + @Schema(description = "面试时间") + private LocalDateTime interviewTime; + + @Schema(description = "面试地址", example = "上海会议室 A") + private String address; + + @Schema(description = "备注", example = "请带作品集") + private String remark; + + @Schema(description = "面试结果", example = "1") + private Integer result; + + @Schema(description = "评价") + private String evaluate; + + @Schema(description = "取消原因") + private String cancelReason; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewResultReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewResultReqVO.java new file mode 100644 index 0000000..90d4b10 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewResultReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.interview; + +import cn.iocoder.yudao.module.hrm.enums.recruit.candidate.HrmRecruitInterviewResultEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.equalsAny; + +@Schema(description = "管理后台 - HRM 招聘面试结果 Request VO") +@Data +public class HrmRecruitInterviewResultReqVO { + + @Schema(description = "面试编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "面试编号不能为空") + private Long id; + + @Schema(description = "面试结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "面试结果不能为空") + private Integer result; + + @Schema(description = "评价", example = "基础扎实") + @Size(max = 255, message = "评价长度不能超过 255 个字符") + private String evaluate; + + @Schema(description = "取消原因", example = "候选人临时改期") + @Size(max = 255, message = "取消原因长度不能超过 255 个字符") + private String cancelReason; + + @AssertTrue(message = "面试结果必须是通过、未通过或取消") + @JsonIgnore + public boolean isResultValid() { + return equalsAny(result, null, HrmRecruitInterviewResultEnum.PASS.getResult(), + HrmRecruitInterviewResultEnum.NOT_PASS.getResult(), HrmRecruitInterviewResultEnum.CANCEL.getResult()); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewSaveReqVO.java new file mode 100644 index 0000000..947c639 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/interview/HrmRecruitInterviewSaveReqVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.interview; + +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 招聘面试新增/修改 Request VO") +@Data +public class HrmRecruitInterviewSaveReqVO { + + @Schema(description = "面试编号", example = "1024") + private Long id; + + @Schema(description = "候选人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "候选人编号不能为空") + private Long candidateId; + + @Schema(description = "面试方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "面试方式不能为空") + @DiffLogField(name = "面试方式") + private Integer type; + + @Schema(description = "面试轮次", example = "1") + private Integer stageNumber; + + @Schema(description = "主面试官员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "主面试官不能为空") + @DiffLogField(name = "主面试官") + private Long interviewEmployeeId; + + @Schema(description = "其他面试官员工编号数组", example = "[2, 3]") + @DiffLogField(name = "其他面试官") + private List otherInterviewEmployeeIds; + + @Schema(description = "面试时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "面试时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @DiffLogField(name = "面试时间") + private LocalDateTime interviewTime; + + @Schema(description = "面试地址", example = "上海会议室 A") + @Size(max = 255, message = "面试地址长度不能超过 255 个字符") + @DiffLogField(name = "面试地址") + private String address; + + @Schema(description = "备注", example = "请带作品集") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + @DiffLogField(name = "备注") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostPageReqVO.java new file mode 100644 index 0000000..70839dc --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostPageReqVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - HRM 招聘职位分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmRecruitPostPageReqVO extends PageParam { + + @Schema(description = "职位名称,模糊匹配", example = "Java") + private String postName; + + @Schema(description = "工作性质", example = "1") + private Integer jobNature; + + @Schema(description = "工作城市地区编号", example = "310115") + private Integer areaId; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "招聘负责人员工编号", example = "200") + private Long ownerEmployeeId; + + @Schema(description = "职位类型编号", example = "100") + private Long postTypeId; + + @Schema(description = "职位状态", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostRespVO.java new file mode 100644 index 0000000..fbe016b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostRespVO.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 招聘职位 Response VO") +@Data +public class HrmRecruitPostRespVO { + + @Schema(description = "招聘职位编号", example = "1024") + private Long id; + + @Schema(description = "职位名称", example = "Java 开发工程师") + private String postName; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "用人部门名称", example = "研发部") + private String deptName; + + @Schema(description = "工作性质", example = "1") + private Integer jobNature; + + @Schema(description = "工作城市地区编号", example = "310115") + private Integer areaId; + + @Schema(description = "工作城市地区名称", example = "上海 上海市 浦东新区") + private String areaName; + + @Schema(description = "招聘人数", example = "3") + private Integer recruitNum; + + @Schema(description = "招聘原因", example = "团队扩编") + private String reason; + + @Schema(description = "工作经验", example = "3") + private Integer workTime; + + @Schema(description = "学历要求", example = "4") + private Integer educationRequire; + + @Schema(description = "最低薪资", example = "20000") + private BigDecimal minSalary; + + @Schema(description = "最高薪资", example = "30000") + private BigDecimal maxSalary; + + @Schema(description = "薪资单位", example = "2") + private Integer salaryUnit; + + @Schema(description = "最小年龄", example = "22") + private Integer minAge; + + @Schema(description = "最大年龄", example = "35") + private Integer maxAge; + + @Schema(description = "最迟到岗时间") + private LocalDateTime latestEntryTime; + + @Schema(description = "招聘负责人员工编号", example = "1") + private Long ownerEmployeeId; + + @Schema(description = "招聘负责人姓名", example = "张三") + private String ownerEmployeeName; + + @Schema(description = "面试官员工编号数组", example = "[1, 2]") + private List interviewEmployeeIds; + + @Schema(description = "面试官姓名数组", example = "[\"张三\", \"李四\"]") + private List interviewEmployeeNames; + + @Schema(description = "职位描述") + private String description; + + @Schema(description = "紧急程度", example = "1") + private Integer emergencyLevel; + + @Schema(description = "职位类型编号", example = "100") + private Long postTypeId; + + @Schema(description = "职位类型名称", example = "研发") + private String postTypeName; + + @Schema(description = "职位状态", example = "1") + private Integer status; + + @Schema(description = "停止原因", example = "岗位暂停") + private String stopReason; + + @Schema(description = "已入职人数", example = "2") + private Long hasEntryNum; + + @Schema(description = "招聘进度百分比", example = "40.00") + private BigDecimal recruitSchedule; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostSaveReqVO.java new file mode 100644 index 0000000..910e5ff --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostSaveReqVO.java @@ -0,0 +1,144 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post; + +import cn.iocoder.yudao.module.system.framework.operatelog.core.AreaParseFunction; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostDO.AGE_UNLIMITED_VALUE; +import static cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostDO.SALARY_NEGOTIABLE_UNIT_VALUE; +import static cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostDO.SALARY_NEGOTIABLE_VALUE; + +@Schema(description = "管理后台 - HRM 招聘职位新增/修改 Request VO") +@Data +public class HrmRecruitPostSaveReqVO { + + @Schema(description = "招聘职位编号", example = "1024") + private Long id; + + @Schema(description = "职位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发工程师") + @NotBlank(message = "职位名称不能为空") + @Size(max = 255, message = "职位名称不能超过 255 个字符") + @DiffLogField(name = "职位名称") + private String postName; + + @Schema(description = "部门编号", example = "100") + @DiffLogField(name = "部门编号") + private Long deptId; + + @Schema(description = "工作性质", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "工作性质不能为空") + @DiffLogField(name = "工作性质") + private Integer jobNature; + + @Schema(description = "工作城市地区编号", example = "310115") + @DiffLogField(name = "工作城市", function = AreaParseFunction.NAME) + private Integer areaId; + + @Schema(description = "招聘人数", example = "3") + @Min(value = 0, message = "招聘人数不能小于 0") + @DiffLogField(name = "招聘人数") + private Integer recruitNum; + + @Schema(description = "招聘原因", example = "团队扩编") + @Size(max = 255, message = "招聘原因不能超过 255 个字符") + @DiffLogField(name = "招聘原因") + private String reason; + + @Schema(description = "工作经验", example = "3") + @DiffLogField(name = "工作经验") + private Integer workTime; + + @Schema(description = "学历要求", example = "4") + @DiffLogField(name = "学历要求") + private Integer educationRequire; + + @Schema(description = "最低薪资", example = "20000") + @Digits(integer = 8, fraction = 2, message = "最低薪资最多 8 位整数和 2 位小数") + @DiffLogField(name = "最低薪资") + private BigDecimal minSalary; + + @Schema(description = "最高薪资", example = "30000") + @Digits(integer = 8, fraction = 2, message = "最高薪资最多 8 位整数和 2 位小数") + @DiffLogField(name = "最高薪资") + private BigDecimal maxSalary; + + @Schema(description = "薪资单位", example = "2") + @DiffLogField(name = "薪资单位") + private Integer salaryUnit; + + @Schema(description = "最小年龄", example = "22") + @DiffLogField(name = "最小年龄") + private Integer minAge; + + @Schema(description = "最大年龄", example = "35") + @DiffLogField(name = "最大年龄") + private Integer maxAge; + + @Schema(description = "最迟到岗时间") + @DiffLogField(name = "最迟到岗时间") + private LocalDateTime latestEntryTime; + + @Schema(description = "负责人员工编号", example = "1") + @DiffLogField(name = "负责人") + private Long ownerEmployeeId; + + @Schema(description = "面试官员工编号数组", example = "[1, 2]") + @DiffLogField(name = "面试官") + private List interviewEmployeeIds; + + @Schema(description = "职位描述", example = "负责核心业务系统设计与开发") + @Size(max = 4000, message = "职位描述不能超过 4000 个字符") + @DiffLogField(name = "职位描述") + private String description; + + @Schema(description = "紧急程度", example = "1") + @DiffLogField(name = "紧急程度") + private Integer emergencyLevel; + + @Schema(description = "职位类型编号", example = "100") + @DiffLogField(name = "职位类型编号") + private Long postTypeId; + + @AssertTrue(message = "薪资范围不正确") + @JsonIgnore + public boolean isSalaryRangeValid() { + boolean hasNegotiableValue = SALARY_NEGOTIABLE_VALUE.equals(getMinSalary()) + || SALARY_NEGOTIABLE_VALUE.equals(getMaxSalary()) || SALARY_NEGOTIABLE_UNIT_VALUE.equals(getSalaryUnit()); + if (hasNegotiableValue) { + return SALARY_NEGOTIABLE_VALUE.equals(getMinSalary()) + && SALARY_NEGOTIABLE_VALUE.equals(getMaxSalary()) + && SALARY_NEGOTIABLE_UNIT_VALUE.equals(getSalaryUnit()); + } + if ((getMinSalary() != null && getMinSalary().signum() < 0) + || (getMaxSalary() != null && getMaxSalary().signum() < 0)) { + return false; + } + return getMinSalary() == null || getMaxSalary() == null || getMinSalary().compareTo(getMaxSalary()) <= 0; + } + + @AssertTrue(message = "年龄范围不正确") + @JsonIgnore + public boolean isAgeRangeValid() { + boolean hasUnlimitedValue = AGE_UNLIMITED_VALUE.equals(getMinAge()) || AGE_UNLIMITED_VALUE.equals(getMaxAge()); + if (hasUnlimitedValue) { + return AGE_UNLIMITED_VALUE.equals(getMinAge()) && AGE_UNLIMITED_VALUE.equals(getMaxAge()); + } + if ((getMinAge() != null && getMinAge() < 0) || (getMaxAge() != null && getMaxAge() < 0)) { + return false; + } + return getMinAge() == null || getMaxAge() == null || getMinAge() <= getMaxAge(); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostStatusReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostStatusReqVO.java new file mode 100644 index 0000000..2eab16b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/HrmRecruitPostStatusReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.recruit.post.HrmRecruitPostStatusEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 招聘职位状态修改 Request VO") +@Data +public class HrmRecruitPostStatusReqVO { + + @Schema(description = "招聘职位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "招聘职位编号不能为空") + private Long id; + + @Schema(description = "职位状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "职位状态不能为空") + @InEnum(value = HrmRecruitPostStatusEnum.class, message = "职位状态必须是 {value}") + private Integer status; + + @Schema(description = "停止原因", example = "岗位暂停") + @Size(max = 255, message = "停止原因不能超过 255 个字符") + private String stopReason; + + @AssertTrue(message = "停止招聘时,停止原因不能为空") + @JsonIgnore + public boolean isStopReasonValid() { + return ObjUtil.notEqual(HrmRecruitPostStatusEnum.STOPPED.getStatus(), status) + || StrUtil.isNotBlank(stopReason); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/type/HrmRecruitPostTypeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/type/HrmRecruitPostTypeRespVO.java new file mode 100644 index 0000000..f2482f2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/recruit/vo/post/type/HrmRecruitPostTypeRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.recruit.vo.post.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 招聘职位类型 Response VO") +@Data +public class HrmRecruitPostTypeRespVO { + + @Schema(description = "职位类型编号", example = "1024") + private Long id; + + @Schema(description = "类型名称", example = "后端开发") + private String name; + + @Schema(description = "父类型编号", example = "0") + private Long parentId; + + @Schema(description = "排序", example = "10") + private Integer sort; + + @Schema(description = "状态,参见 common_status", example = "0") + private Integer status; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryChangeRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryChangeRecordController.java new file mode 100644 index 0000000..f8f7fcc --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryChangeRecordController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changerecord.HrmSalaryChangeRecordRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.employee.HrmSalaryChangeRecordDO; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryChangeRecordTypeEnum; +import cn.iocoder.yudao.module.hrm.service.salary.employee.HrmSalaryChangeRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - HRM 定薪/调薪记录") +@RestController +@RequestMapping("/hrm/salary/change-record") +@Validated +public class HrmSalaryChangeRecordController { + + @Resource + private HrmSalaryChangeRecordService salaryChangeRecordService; + + @GetMapping("/get") + @Operation(summary = "获得定薪/调薪记录") + @Parameter(name = "id", description = "记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:query')") + public CommonResult getSalaryChangeRecord(@RequestParam("id") Long id) { + return success(buildSalaryChangeRecordRespVO( + salaryChangeRecordService.getSalaryChangeRecord(id))); + } + + @GetMapping("/list") + @Operation(summary = "获得定薪/调薪记录列表") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:query')") + public CommonResult> getSalaryChangeRecordList( + @RequestParam("employeeId") Long employeeId) { + return success(convertList(salaryChangeRecordService.getSalaryChangeRecordList(employeeId), + this::buildSalaryChangeRecordRespVO)); + } + + @PutMapping("/cancel") + @Operation(summary = "取消定薪/调薪记录") + @Parameter(name = "id", description = "记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:update')") + public CommonResult cancelSalaryChangeRecord(@RequestParam("id") Long id) { + salaryChangeRecordService.cancelSalaryChangeRecord(id); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除定薪/调薪记录") + @Parameter(name = "id", description = "记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:change-record:delete')") + public CommonResult deleteSalaryChangeRecord(@RequestParam("id") Long id) { + salaryChangeRecordService.deleteSalaryChangeRecord(id); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private HrmSalaryChangeRecordRespVO buildSalaryChangeRecordRespVO( + HrmSalaryChangeRecordDO changeRecord) { + if (changeRecord == null) { + return null; + } + HrmSalaryChangeRecordRespVO respVO = BeanUtils.toBean(changeRecord, HrmSalaryChangeRecordRespVO.class); + respVO.setRecordType(changeRecord.getType()); + respVO.setChangeReason(changeRecord.getReason()); + HrmSalaryChangeRecordTypeEnum recordType = + HrmSalaryChangeRecordTypeEnum.valueOf(changeRecord.getType()); + respVO.setRecordTypeName(recordType == null ? null : recordType.getName()); + return respVO; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryChangeTemplateController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryChangeTemplateController.java new file mode 100644 index 0000000..eaf16f7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryChangeTemplateController.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changetemplate.HrmSalaryChangeTemplateRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changetemplate.HrmSalaryChangeTemplateSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryChangeTemplateDO; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryChangeTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 调薪模板") +@RestController +@RequestMapping("/hrm/salary/change-template") +@Validated +public class HrmSalaryChangeTemplateController { + + @Resource + private HrmSalaryChangeTemplateService salaryChangeTemplateService; + + @PostMapping("/create") + @Operation(summary = "创建调薪模板") + @PreAuthorize("@ss.hasPermission('hrm:salary:change-template:create')") + public CommonResult createSalaryChangeTemplate( + @Valid @RequestBody HrmSalaryChangeTemplateSaveReqVO createReqVO) { + return success(salaryChangeTemplateService.createSalaryChangeTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新调薪模板") + @PreAuthorize("@ss.hasPermission('hrm:salary:change-template:update')") + public CommonResult updateSalaryChangeTemplate( + @Valid @RequestBody HrmSalaryChangeTemplateSaveReqVO updateReqVO) { + salaryChangeTemplateService.updateSalaryChangeTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除调薪模板") + @Parameter(name = "id", description = "调薪模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:change-template:delete')") + public CommonResult deleteSalaryChangeTemplate(@RequestParam("id") Long id) { + salaryChangeTemplateService.deleteSalaryChangeTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得调薪模板") + @Parameter(name = "id", description = "调薪模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:change-template:query')") + public CommonResult getSalaryChangeTemplate(@RequestParam("id") Long id) { + HrmSalaryChangeTemplateDO template = salaryChangeTemplateService.getSalaryChangeTemplate(id); + return success(BeanUtils.toBean(template, HrmSalaryChangeTemplateRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得调薪模板列表") + @PreAuthorize("@ss.hasPermission('hrm:salary:change-template:query') " + + "or @ss.hasPermission('hrm:salary:employee-info:query')") + public CommonResult> getSalaryChangeTemplateList() { + List list = salaryChangeTemplateService.getSalaryChangeTemplateList(); + return success(BeanUtils.toBean(list, HrmSalaryChangeTemplateRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryConfigController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryConfigController.java new file mode 100644 index 0000000..5e5d28c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryConfigController.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.config.HrmSalaryConfigCreateReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.config.HrmSalaryConfigRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.config.HrmSalaryConfigUpdateReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryConfigDO; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - HRM 薪资配置") +@RestController +@RequestMapping("/hrm/salary/config") +@Validated +public class HrmSalaryConfigController { + + @Resource + private HrmSalaryConfigService salaryConfigService; + + @PostMapping("/create") + @Operation(summary = "创建薪资配置") + @PreAuthorize("@ss.hasPermission('hrm:salary:config:update')") + public CommonResult createSalaryConfig(@Valid @RequestBody HrmSalaryConfigCreateReqVO reqVO) { + return success(salaryConfigService.createSalaryConfig(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新薪资配置") + @PreAuthorize("@ss.hasPermission('hrm:salary:config:update')") + public CommonResult updateSalaryConfig(@Valid @RequestBody HrmSalaryConfigUpdateReqVO reqVO) { + salaryConfigService.updateSalaryConfig(reqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得薪资配置") + @PreAuthorize("@ss.hasPermission('hrm:salary:config:query')") + public CommonResult getSalaryConfig() { + HrmSalaryConfigDO config = salaryConfigService.getSalaryConfig(); + return success(BeanUtils.toBean(config, HrmSalaryConfigRespVO.class)); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryEmployeeInfoController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryEmployeeInfoController.java new file mode 100644 index 0000000..287d94d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryEmployeeInfoController.java @@ -0,0 +1,343 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeStatusCountRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo.HrmSalaryEmployeeInfoUpdateListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo.HrmSalaryEmployeeInfoUpdateListRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo.HrmSalaryEmployeeInfoPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo.HrmSalaryEmployeeInfoUpdateReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo.HrmSalaryEmployeeInfoRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo.HrmSalaryEmployeeInfoImportRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.employee.HrmSalaryEmployeeInfoDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.monthrecord.HrmSalaryMonthRecordDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryOptionDO; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusTabEnum; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.salary.employee.HrmSalaryEmployeeInfoService; +import cn.iocoder.yudao.module.hrm.service.salary.monthrecord.HrmSalaryMonthRecordService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryEmployeeInfoChangeTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 员工薪资信息") +@RestController +@RequestMapping("/hrm/salary/employee-info") +@Validated +public class HrmSalaryEmployeeInfoController { + + @Resource + private HrmSalaryEmployeeInfoService salaryEmployeeInfoService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmSalaryMonthRecordService salaryMonthRecordService; + + @Resource + private DeptApi deptApi; + + @GetMapping("/page") + @Operation(summary = "获得员工薪资信息分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:query')") + public CommonResult> getSalaryEmployeeInfoPage( + @Validated HrmSalaryEmployeeInfoPageReqVO reqVO) { + PageResult pageResult = salaryEmployeeInfoService.getSalaryEmployeeInfoPage(reqVO); + return success(buildSalaryEmployeeInfoRespVOPageResult(pageResult)); + } + + @GetMapping("/status-count") + @Operation(summary = "获得员工薪资信息状态数量") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:query')") + public CommonResult> getSalaryEmployeeInfoStatusCount( + @Validated HrmSalaryEmployeeInfoPageReqVO reqVO) { + Map countMap = salaryEmployeeInfoService.getSalaryEmployeeInfoStatusCount(reqVO); + return success(convertList(Arrays.asList(HrmEmployeeStatusTabEnum.values()), + status -> new HrmEmployeeStatusCountRespVO(status.getStatus(), + countMap.getOrDefault(status.getStatus(), 0L)))); + } + + @GetMapping("/get") + @Operation(summary = "获得员工薪资信息") + @Parameter(name = "employeeId", description = "员工编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:query')") + public CommonResult getSalaryEmployeeInfo( + @RequestParam("employeeId") Long employeeId) { + HrmEmployeeDO employee = employeeService.getEmployee(employeeId); + if (employee == null) { + return success(null); + } + HrmSalaryEmployeeInfoDO salaryEmployeeInfo = + salaryEmployeeInfoService.getSalaryEmployeeInfoByEmployeeId(employeeId); + Map deptMap = deptApi.getDeptMap(Collections.singleton(employee.getDeptId())); + return success(buildSalaryEmployeeInfoRespVO(employee, salaryEmployeeInfo, deptMap)); + } + + @GetMapping("/get-adjustment-min-effect-date") + @Operation(summary = "获得最早调薪生效日期") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:update')") + public CommonResult getSalaryAdjustmentMinEffectDate() { + return success(getSalaryAdjustmentMinEffectDateValue()); + } + + private LocalDate getSalaryAdjustmentMinEffectDateValue() { + HrmSalaryMonthRecordDO monthRecord = salaryMonthRecordService.getLastMonthRecord(); + if (monthRecord == null || monthRecord.getYear() == null || monthRecord.getMonth() == null) { + return null; + } + return YearMonth.of(monthRecord.getYear(), monthRecord.getMonth()).atDay(1); + } + + @PutMapping("/update") + @Operation(summary = "修改员工薪资信息") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:update')") + public CommonResult updateSalaryEmployeeInfo( + @Valid @RequestBody HrmSalaryEmployeeInfoUpdateReqVO reqVO) { + return success(salaryEmployeeInfoService.updateSalaryEmployeeInfo(reqVO)); + } + + @PutMapping("/update-list") + @Operation(summary = "批量调薪") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:update')") + public CommonResult updateSalaryEmployeeInfoList( + @Valid @RequestBody HrmSalaryEmployeeInfoUpdateListReqVO reqVO) { + return success(salaryEmployeeInfoService.updateSalaryEmployeeInfoList(reqVO)); + } + + @GetMapping("/get-fix-import-template") + @Operation(summary = "获得薪资档案定薪导入模板") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:import')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getFixImportTemplate(HttpServletResponse response) throws IOException { + List options = salaryEmployeeInfoService.getSalaryImportOptionList(); + List> head = buildFixSalaryImportHead(options); + List> data = buildFixSalaryImportTemplateData(options); + ExcelUtils.write(response, "薪资档案定薪导入模板.xls", "薪资档案", head, data); + } + + @PostMapping("/import-fix") + @Operation(summary = "导入薪资档案定薪") + @Parameter(name = "file", description = "Excel 文件", required = true) + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:import')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult importFixExcel(@RequestParam("file") MultipartFile file) + throws IOException { + return success(salaryEmployeeInfoService.importFixSalaryList(ExcelUtils.read(file))); + } + + @GetMapping("/get-change-import-template") + @Operation(summary = "获得薪资档案调薪导入模板") + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:import')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getChangeImportTemplate(HttpServletResponse response) throws IOException { + List options = salaryEmployeeInfoService.getSalaryImportOptionList(); + List> head = buildChangeSalaryImportHead(options); + List> data = buildChangeSalaryImportTemplateData(options); + ExcelUtils.write(response, "薪资档案调薪导入模板.xls", "薪资档案", head, data); + } + + @PostMapping("/import-change") + @Operation(summary = "导入薪资档案调薪") + @Parameter(name = "file", description = "Excel 文件", required = true) + @PreAuthorize("@ss.hasPermission('hrm:salary:employee-info:import')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult importChangeExcel(@RequestParam("file") MultipartFile file) + throws IOException { + return success(salaryEmployeeInfoService.importChangeSalaryList(ExcelUtils.read(file))); + } + + // ==================== 拼接 VO ==================== + + private PageResult buildSalaryEmployeeInfoRespVOPageResult( + PageResult pageResult) { + if (CollUtil.isEmpty(pageResult.getList())) { + return PageResult.empty(pageResult.getTotal()); + } + // 1. 获取关联数据 + Map salaryEmployeeInfoMap = + salaryEmployeeInfoService.getSalaryEmployeeInfoMap( + convertSet(pageResult.getList(), HrmEmployeeDO::getId)); + Map deptMap = deptApi.getDeptMap( + convertSet(pageResult.getList(), HrmEmployeeDO::getDeptId)); + + // 2. 拼接响应 + return new PageResult<>(convertList(pageResult.getList(), employee -> buildSalaryEmployeeInfoRespVO( + employee, salaryEmployeeInfoMap.get(employee.getId()), deptMap)), pageResult.getTotal()); + } + + private HrmSalaryEmployeeInfoRespVO buildSalaryEmployeeInfoRespVO( + HrmEmployeeDO employee, HrmSalaryEmployeeInfoDO salaryEmployeeInfo, Map deptMap) { + // 1. 拼接员工基础信息 + HrmSalaryEmployeeInfoRespVO respVO = salaryEmployeeInfo == null + ? new HrmSalaryEmployeeInfoRespVO() + : BeanUtils.toBean(salaryEmployeeInfo, HrmSalaryEmployeeInfoRespVO.class); + respVO.setEmployeeId(employee.getId()).setEmployeeName(employee.getName()) + .setJobNumber(employee.getJobNumber()).setMobile(employee.getMobile()) + .setDeptId(employee.getDeptId()).setPostName(employee.getPostName()) + .setEntryStatus(employee.getEntryStatus()).setStatus(employee.getStatus()) + .setEntryTime(employee.getEntryTime()).setRegularTime(employee.getRegularTime()); + MapUtils.findAndThen(deptMap, employee.getDeptId(), + dept -> respVO.setDeptName(dept.getName())); + + // 2. 拼接当前薪资信息 + if (salaryEmployeeInfo == null) { + respVO.setChangeType(HrmSalaryEmployeeInfoChangeTypeEnum.UNSET.getType()) + .setSalaryOptions(Collections.emptyList()).setProbationSalaryOptions(Collections.emptyList()); + return respVO; + } + return respVO; + } + + // ==================== Excel 导入导出 ==================== + + private List> buildFixSalaryImportHead(List options) { + List> head = buildSalaryImportEmployeeHead(); + options.forEach(option -> head.add(Collections.singletonList("试用期-" + option.getName()))); + options.forEach(option -> head.add(Collections.singletonList("正式-" + option.getName()))); + head.add(Collections.singletonList("备注")); + return head; + } + + private List> buildFixSalaryImportTemplateData(List options) { + List employees = getActiveEmployeeList(); + List> rows = convertList(employees, employee -> { + List row = buildSalaryImportEmployeeRow(employee); + options.forEach(option -> row.add(BigDecimal.ZERO)); + options.forEach(option -> row.add(BigDecimal.ZERO)); + row.add(""); + return row; + }); + if (CollUtil.isEmpty(rows)) { + List row = new ArrayList<>(Arrays.asList( + "张三", "HRM001", 100L, "Java 工程师")); + options.forEach(option -> row.add(BigDecimal.ZERO)); + options.forEach(option -> row.add(BigDecimal.ZERO)); + row.add("示例定薪"); + rows.add(row); + } + return rows; + } + + private List> buildChangeSalaryImportHead(List options) { + List> head = buildSalaryImportBaseHead("调薪原因"); + options.forEach(option -> { + head.add(Collections.singletonList("试用期-" + option.getName() + "-调整前")); + head.add(Collections.singletonList("试用期-" + option.getName() + "-调整后")); + }); + options.forEach(option -> { + head.add(Collections.singletonList("正式-" + option.getName() + "-调整前")); + head.add(Collections.singletonList("正式-" + option.getName() + "-调整后")); + }); + head.add(Collections.singletonList("备注")); + return head; + } + + private List> buildChangeSalaryImportTemplateData(List options) { + List employees = getActiveEmployeeList(); + Map salaryEmployeeInfoMap = + salaryEmployeeInfoService.getSalaryEmployeeInfoMap(convertSet(employees, HrmEmployeeDO::getId)); + LocalDate effectDate = getSalaryAdjustmentMinEffectDateValue(); + if (effectDate == null || effectDate.isBefore(LocalDate.now())) { + effectDate = LocalDate.now(); + } + LocalDate finalEffectDate = effectDate; + return convertList(employees, employee -> { + HrmSalaryEmployeeInfoDO salaryEmployeeInfo = salaryEmployeeInfoMap.get(employee.getId()); + List row = buildSalaryImportBaseRow(employee, finalEffectDate, "转正"); + appendChangeOptionTemplateValues(row, options, + buildSalaryOptionValueMap(salaryEmployeeInfo == null + ? Collections.emptyList() : salaryEmployeeInfo.getProbationSalaryOptions())); + appendChangeOptionTemplateValues(row, options, + buildSalaryOptionValueMap(salaryEmployeeInfo == null + ? Collections.emptyList() : salaryEmployeeInfo.getSalaryOptions())); + row.add(""); + return row; + }); + } + + private List getActiveEmployeeList() { + return employeeService.getEmployeeList(new HrmEmployeeListReqVO() + .setStatusCategory(HrmEmployeeStatusTabEnum.ACTIVE.getStatus())); + } + + private List> buildSalaryImportBaseHead(String reasonName) { + List> head = buildSalaryImportEmployeeHead(); + head.add(Collections.singletonList("生效日期")); + head.add(Collections.singletonList(reasonName)); + return head; + } + + private List> buildSalaryImportEmployeeHead() { + return new ArrayList<>(Arrays.asList( + Collections.singletonList("员工姓名"), + Collections.singletonList("工号"), + Collections.singletonList("部门编号"), + Collections.singletonList("岗位"))); + } + + private List buildSalaryImportEmployeeRow(HrmEmployeeDO employee) { + return new ArrayList<>(Arrays.asList( + employee.getName(), employee.getJobNumber(), employee.getDeptId(), employee.getPostName())); + } + + private List buildSalaryImportBaseRow(HrmEmployeeDO employee, LocalDate effectDate, String changeReason) { + List row = buildSalaryImportEmployeeRow(employee); + row.add(effectDate.toString()); + row.add(changeReason); + return row; + } + + private Map buildSalaryOptionValueMap( + List options) { + return convertMap(options, HrmSalaryEmployeeInfoDO.SalaryOption::getCode, + option -> option.getValue() == null ? BigDecimal.ZERO : option.getValue()); + } + + private void appendChangeOptionTemplateValues(List row, List options, + Map valueMap) { + for (HrmSalaryOptionDO option : options) { + BigDecimal value = valueMap.getOrDefault(option.getCode(), BigDecimal.ZERO); + row.add(value); + row.add(value); + } + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryGroupController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryGroupController.java new file mode 100644 index 0000000..0dae418 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryGroupController.java @@ -0,0 +1,141 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.group.HrmSalaryGroupPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.group.HrmSalaryGroupRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.group.HrmSalaryGroupSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryGroupDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryTaxRuleDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryGroupService; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryTaxRuleService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - HRM 薪资组") +@RestController +@RequestMapping("/hrm/salary/group") +@Validated +public class HrmSalaryGroupController { + + @Resource + private HrmSalaryGroupService salaryGroupService; + @Resource + private HrmSalaryTaxRuleService salaryTaxRuleService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private DeptApi deptApi; + + @PostMapping("/create") + @Operation(summary = "创建薪资组") + @PreAuthorize("@ss.hasPermission('hrm:salary:group:create')") + public CommonResult createSalaryGroup(@Valid @RequestBody HrmSalaryGroupSaveReqVO reqVO) { + return success(salaryGroupService.createSalaryGroup(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改薪资组") + @PreAuthorize("@ss.hasPermission('hrm:salary:group:update')") + public CommonResult updateSalaryGroup(@Valid @RequestBody HrmSalaryGroupSaveReqVO reqVO) { + salaryGroupService.updateSalaryGroup(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除薪资组") + @Parameter(name = "id", description = "薪资组编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:group:delete')") + public CommonResult deleteSalaryGroup(@RequestParam("id") Long id) { + salaryGroupService.deleteSalaryGroup(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得薪资组") + @Parameter(name = "id", description = "薪资组编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:group:query')") + public CommonResult getSalaryGroup(@RequestParam("id") Long id) { + return success(buildSalaryGroupRespVO(salaryGroupService.getSalaryGroup(id))); + } + + @GetMapping("/page") + @Operation(summary = "获得薪资组分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:group:query')") + public CommonResult> getSalaryGroupPage( + @Validated HrmSalaryGroupPageReqVO reqVO) { + PageResult pageResult = salaryGroupService.getSalaryGroupPage(reqVO); + return success(new PageResult<>(buildSalaryGroupRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得薪资组精简列表") + @PreAuthorize("@ss.hasPermission('hrm:salary:group:query')") + public CommonResult> getSalaryGroupSimpleList() { + return success(convertList(salaryGroupService.getSalaryGroupList(), + salaryGroup -> new HrmSalaryGroupRespVO().setId(salaryGroup.getId()) + .setName(salaryGroup.getName()).setTaxRuleId(salaryGroup.getTaxRuleId()))); + } + + // ==================== 拼接 VO ==================== + + private HrmSalaryGroupRespVO buildSalaryGroupRespVO(HrmSalaryGroupDO group) { + if (group == null) { + return null; + } + return CollUtil.getFirst(buildSalaryGroupRespVOList(Collections.singletonList(group))); + } + + private List buildSalaryGroupRespVOList(List salaryGroups) { + if (CollUtil.isEmpty(salaryGroups)) { + return Collections.emptyList(); + } + // 1.1 批量查询计税规则 + Map taxRuleMap = salaryTaxRuleService.getSalaryTaxRuleMap( + convertSet(salaryGroups, HrmSalaryGroupDO::getTaxRuleId)); + // 1.2 批量查询部门和员工 + Map deptMap = deptApi.getDeptMap( + convertSetByFlatMap(salaryGroups, HrmSalaryGroupDO::getDeptIds, Collection::stream)); + Map employeeMap = employeeService.getEmployeeMap( + convertSetByFlatMap(salaryGroups, HrmSalaryGroupDO::getEmployeeIds, Collection::stream)); + + // 2. 拼接响应 + return BeanUtils.toBean(salaryGroups, HrmSalaryGroupRespVO.class, vo -> { + MapUtils.findAndThen(taxRuleMap, vo.getTaxRuleId(), taxRule -> vo.setTaxRuleName(taxRule.getName())); + vo.setDeptNames(convertList(convertList(vo.getDeptIds(), deptMap::get), DeptRespDTO::getName)); + vo.setEmployeeNames(convertList(convertList(vo.getEmployeeIds(), employeeMap::get), + HrmEmployeeDO::getName)); + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryMonthEmployeeRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryMonthEmployeeRecordController.java new file mode 100644 index 0000000..03bd06b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryMonthEmployeeRecordController.java @@ -0,0 +1,153 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee.HrmSalaryEmployeeMonthRecordPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee.HrmSalaryMonthEmployeeRecordListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee.HrmSalaryMonthEmployeeRecordPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee.HrmSalaryMonthEmployeeRecordRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee.HrmSalaryMonthEmployeeRecordUpdateReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee.HrmSalaryPerformanceCoefficientReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.monthrecord.HrmSalaryMonthEmployeeRecordDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.salary.monthrecord.HrmSalaryMonthEmployeeRecordService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 员工月度工资") +@RestController +@RequestMapping("/hrm/salary/month-employee-record") +@Validated +public class HrmSalaryMonthEmployeeRecordController { + + @Resource + private HrmSalaryMonthEmployeeRecordService monthEmployeeRecordService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private DeptApi deptApi; + + @PutMapping("/update-list") + @Operation(summary = "批量修改员工工资项") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:update')") + public CommonResult updateMonthEmployeeRecordList( + @NotEmpty(message = "员工工资记录不能为空") + @RequestBody List<@Valid HrmSalaryMonthEmployeeRecordUpdateReqVO> reqVOs) { + monthEmployeeRecordService.updateMonthEmployeeRecordList(reqVOs); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得员工月度工资分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult> getMonthEmployeeRecordPage( + @Valid HrmSalaryMonthEmployeeRecordPageReqVO reqVO) { + PageResult pageResult = + monthEmployeeRecordService.getMonthEmployeeRecordPage(reqVO); + return success(new PageResult<>(buildMonthEmployeeRecordRespVOList(pageResult.getList()), + pageResult.getTotal())); + } + + @GetMapping("/employee-page") + @Operation(summary = "获得指定员工的月度工资分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult> getEmployeeMonthRecordPage( + @Valid HrmSalaryEmployeeMonthRecordPageReqVO reqVO) { + PageResult pageResult = + monthEmployeeRecordService.getEmployeeMonthRecordPage(reqVO); + return success(new PageResult<>(buildMonthEmployeeRecordRespVOList(pageResult.getList()), + pageResult.getTotal())); + } + + @GetMapping("/list") + @Operation(summary = "获得员工月度工资列表") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult> getMonthEmployeeRecordList( + @Valid HrmSalaryMonthEmployeeRecordListReqVO reqVO) { + return success(buildMonthEmployeeRecordRespVOList( + monthEmployeeRecordService.getMonthEmployeeRecordList(reqVO))); + } + + @GetMapping("/change-count") + @Operation(summary = "获得工资表员工异动分类数量") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult> getMonthEmployeeChangeCount( + @Valid HrmSalaryMonthEmployeeRecordPageReqVO reqVO) { + return success(monthEmployeeRecordService.getMonthEmployeeChangeCount(reqVO)); + } + + @PostMapping("/performance-coefficients") + @Operation(summary = "获得员工绩效系数") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult> getPerformanceCoefficientMap( + @Valid @RequestBody HrmSalaryPerformanceCoefficientReqVO reqVO) { + return success(monthEmployeeRecordService.getPerformanceCoefficientMap(reqVO)); + } + + // ==================== 拼接 VO ==================== + + private List buildMonthEmployeeRecordRespVOList( + List employeeRecords) { + if (CollUtil.isEmpty(employeeRecords)) { + return Collections.emptyList(); + } + + // 1.1 批量查询员工和部门 + Set employeeIds = convertSet(employeeRecords, HrmSalaryMonthEmployeeRecordDO::getEmployeeId); + Map employeeMap = employeeService.getEmployeeMap(employeeIds); + Map deptMap = deptApi.getDeptMap( + convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId)); + // 1.2 批量查询绩效系数和薪资项 + HrmSalaryMonthEmployeeRecordDO firstRecord = CollUtil.getFirst(employeeRecords); + boolean sameMonth = employeeRecords.stream().allMatch(record -> + Objects.equals(record.getYear(), firstRecord.getYear()) + && Objects.equals(record.getMonth(), firstRecord.getMonth())); + Map coefficientMap = sameMonth + ? monthEmployeeRecordService.getPerformanceCoefficientMap( + new HrmSalaryPerformanceCoefficientReqVO().setYear(firstRecord.getYear()) + .setMonth(firstRecord.getMonth()).setEmployeeIds(new ArrayList<>(employeeIds))) + : Collections.emptyMap(); + // 2. 拼接响应 + List respVOs = BeanUtils.toBean( + employeeRecords, HrmSalaryMonthEmployeeRecordRespVO.class); + for (HrmSalaryMonthEmployeeRecordRespVO respVO : respVOs) { + MapUtils.findAndThen(employeeMap, respVO.getEmployeeId(), employee -> { + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber()) + .setDeptId(employee.getDeptId()).setPostName(employee.getPostName()); + MapUtils.findAndThen(deptMap, employee.getDeptId(), + dept -> respVO.setDeptName(dept.getName())); + }); + respVO.setPerformanceCoefficient(coefficientMap.get(respVO.getEmployeeId())); + } + return respVOs; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryMonthRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryMonthRecordController.java new file mode 100644 index 0000000..ece5bb9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryMonthRecordController.java @@ -0,0 +1,352 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics.HrmAttendanceMonthRecordRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee.HrmSalaryMonthEmployeeRecordListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.HrmSalaryMonthRecordPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.HrmSalaryMonthRecordRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.HrmSalaryPayrollReadinessRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryGroupDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.monthrecord.HrmSalaryMonthEmployeeRecordDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.monthrecord.HrmSalaryMonthRecordDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryTaxRuleDO; +import cn.iocoder.yudao.module.hrm.service.attendance.statistics.HrmAttendanceStatisticsService; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryGroupService; +import cn.iocoder.yudao.module.hrm.service.salary.monthrecord.HrmSalaryMonthEmployeeRecordService; +import cn.iocoder.yudao.module.hrm.service.salary.monthrecord.HrmSalaryMonthRecordService; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryTaxRuleService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.config.HrmSalaryTaxTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 月度工资表") +@RestController +@RequestMapping("/hrm/salary/month-record") +@Validated +public class HrmSalaryMonthRecordController { + + @Resource + private HrmSalaryMonthRecordService salaryMonthRecordService; + @Resource + private HrmSalaryMonthEmployeeRecordService monthEmployeeRecordService; + @Resource + private HrmSalaryGroupService salaryGroupService; + @Resource + private HrmSalaryTaxRuleService salaryTaxRuleService; + @Resource + private HrmEmployeeService employeeService; + @Resource + private HrmAttendanceStatisticsService attendanceStatisticsService; + + @Resource + private DeptApi deptApi; + + @PostMapping("/create-next") + @Operation(summary = "创建下月工资表") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:create')") + public CommonResult createNextMonthRecord() { + return success(salaryMonthRecordService.createNextMonthRecord()); + } + + @PostMapping("/compute") + @Operation(summary = "核算工资表") + @Parameter(name = "id", description = "工资表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:compute')") + public CommonResult computeMonthRecord(@RequestParam("id") Long id) { + salaryMonthRecordService.computeMonthRecord(id); + return success(true); + } + + @PostMapping(value = "/compute-import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "带导入文件核算工资表") + @Parameters({ + @Parameter(name = "id", description = "工资表编号", required = true, example = "1024"), + @Parameter(name = "syncInsuranceData", description = "是否同步社保数据", example = "true"), + @Parameter(name = "syncAttendanceData", description = "是否同步考勤数据", example = "false"), + @Parameter(name = "attendanceFile", description = "考勤数据文件"), + @Parameter(name = "additionalDeductionFile", description = "专项附加扣除数据文件"), + @Parameter(name = "cumulativeTaxFile", description = "上月个税累计数据文件") + }) + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:compute')") + @ApiAccessLog(operateType = OperateTypeEnum.IMPORT) + public CommonResult computeMonthRecordWithImport( + @RequestParam("id") Long id, + @RequestParam(value = "syncInsuranceData", defaultValue = "true") boolean syncInsuranceData, + @RequestParam(value = "syncAttendanceData", defaultValue = "false") boolean syncAttendanceData, + @RequestParam(value = "attendanceFile", required = false) MultipartFile attendanceFile, + @RequestParam(value = "additionalDeductionFile", required = false) MultipartFile additionalDeductionFile, + @RequestParam(value = "cumulativeTaxFile", required = false) MultipartFile cumulativeTaxFile) + throws IOException { + salaryMonthRecordService.computeMonthRecord(id, syncInsuranceData, syncAttendanceData, + ExcelUtils.read(attendanceFile), + ExcelUtils.read(additionalDeductionFile), + ExcelUtils.read(cumulativeTaxFile)); + return success(true); + } + + @GetMapping("/get-attendance-import-template") + @Operation(summary = "获得工资表考勤导入模板") + @Parameter(name = "monthRecordId", description = "月度工资表编号", example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:compute')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getAttendanceImportTemplate( + @RequestParam(value = "monthRecordId", required = false) Long monthRecordId, + HttpServletResponse response) throws IOException { + List employees = getPayrollImportEmployeeList(monthRecordId, false); + List> data; + if (monthRecordId == null) { + data = buildImportTemplateData(employees, 8); + } else { + HrmSalaryMonthRecordDO monthRecord = salaryMonthRecordService.getMonthRecord(monthRecordId); + if (monthRecord == null) { + data = buildImportTemplateData(employees, 8); + } else { + Map attendanceMap = convertMap( + attendanceStatisticsService.getAttendanceMonthRecordList(monthRecord.getYear(), + monthRecord.getMonth(), convertList(employees, HrmEmployeeDO::getId)), + HrmAttendanceMonthRecordRespVO::getEmployeeId); + data = convertList(employees, employee -> { + List row = buildImportEmployeeRow(employee); + HrmAttendanceMonthRecordRespVO attendance = attendanceMap.get(employee.getId()); + row.add(BigDecimal.ZERO); + row.add(attendance == null ? BigDecimal.ZERO : attendance.getLateDeductAmount()); + row.add(attendance == null ? BigDecimal.ZERO : attendance.getEarlyDeductAmount()); + row.add(attendance == null ? BigDecimal.ZERO : attendance.getAbsenteeismDeductAmount()); + row.add(BigDecimal.ZERO); + row.add(attendance == null ? BigDecimal.ZERO : attendance.getMisscardDeductAmount()); + row.add(BigDecimal.ZERO); + row.add(attendance == null ? BigDecimal.ZERO : attendance.getActualDays()); + return row; + }); + } + } + List> head = buildImportHead(Arrays.asList( + "加班工资", "迟到扣款", "早退扣款", "旷工扣款", "假期扣款", + "缺卡扣款", "综合扣款", "实际计薪天数")); + ExcelUtils.write(response, "月度工资考勤导入模板.xls", "月度工资", head, data); + } + + @GetMapping("/get-cumulative-tax-import-template") + @Operation(summary = "获得工资表上月个税累计导入模板") + @Parameter(name = "monthRecordId", description = "月度工资表编号", example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:compute')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getCumulativeTaxImportTemplate( + @RequestParam(value = "monthRecordId", required = false) Long monthRecordId, + HttpServletResponse response) throws IOException { + List> head = buildImportHead(Arrays.asList( + "累计收入额(截至上月)", "累计减除费用(截至上月)", + "累计专项扣除(截至上月)", "累计已预缴税额")); + List employees = getPayrollImportEmployeeList(monthRecordId, true); + List> data = buildImportTemplateData(employees, 4); + ExcelUtils.write(response, "月度工资上月个税累计导入模板.xls", "月度工资", head, data); + } + + @GetMapping("/get-additional-deduction-import-template") + @Operation(summary = "获得工资表专项附加扣除导入模板") + @Parameter(name = "monthRecordId", description = "月度工资表编号", example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:compute')") + @ApiAccessLog(operateType = OperateTypeEnum.EXPORT) + public void getAdditionalDeductionImportTemplate( + @RequestParam(value = "monthRecordId", required = false) Long monthRecordId, + HttpServletResponse response) throws IOException { + List> head = buildImportHead(Arrays.asList( + "累计子女教育", "累计住房租金", "累计住房贷款利息", "累计赡养老人", "累计继续教育")); + List employees = getPayrollImportEmployeeList(monthRecordId, true); + List> data = buildImportTemplateData(employees, 5); + ExcelUtils.write(response, "月度工资专项附加扣除导入模板.xls", "月度工资", head, data); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除月度工资表") + @Parameter(name = "id", description = "工资表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:delete')") + public CommonResult deleteMonthRecord(@RequestParam("id") Long id) { + salaryMonthRecordService.deleteMonthRecord(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得月度工资表") + @Parameter(name = "id", description = "月度工资表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult getMonthRecord(@RequestParam("id") Long id) { + HrmSalaryMonthRecordDO monthRecord = salaryMonthRecordService.getMonthRecord(id); + return success(buildMonthRecordRespVO(monthRecord)); + } + + @GetMapping("/page") + @Operation(summary = "获得月度工资表分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult> getMonthRecordPage( + @Valid HrmSalaryMonthRecordPageReqVO reqVO) { + PageResult pageResult = salaryMonthRecordService.getMonthRecordPage(reqVO); + return success(BeanUtils.toBean(pageResult, HrmSalaryMonthRecordRespVO.class)); + } + + @GetMapping("/last") + @Operation(summary = "获得最近月度工资表") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult getLastMonthRecord() { + HrmSalaryMonthRecordDO monthRecord = salaryMonthRecordService.getLastMonthRecord(); + return success(buildMonthRecordRespVO(monthRecord)); + } + + @GetMapping("/payroll-readiness") + @Operation(summary = "获得工资核算准备情况") + @Parameter(name = "monthRecordId", description = "月度工资表编号", example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult getPayrollReadiness( + @RequestParam(value = "monthRecordId", required = false) Long monthRecordId) { + HrmSalaryPayrollReadinessRespVO readiness = + salaryMonthRecordService.getPayrollReadiness(monthRecordId); + if (readiness == null) { + return success(null); + } + fillPayrollReadinessEmployeeDeptName(readiness.getNoSalaryEmployees()); + fillPayrollReadinessEmployeeDeptName(readiness.getNoSalaryGroupEmployees()); + return success(readiness); + } + + @GetMapping("/option-summary") + @Operation(summary = "获得工资项汇总") + @PreAuthorize("@ss.hasPermission('hrm:salary:month-record:query')") + public CommonResult> getMonthOptionSummary( + @Valid HrmSalaryMonthEmployeeRecordListReqVO reqVO) { + List employeeRecords = + monthEmployeeRecordService.getMonthEmployeeRecordList(reqVO); + List optionSummary = + monthEmployeeRecordService.getMonthOptionSummary(employeeRecords); + return success(BeanUtils.toBean(optionSummary, HrmSalaryOptionValueVO.class)); + } + + // ==================== 拼接 VO ==================== + + private HrmSalaryMonthRecordRespVO buildMonthRecordRespVO(HrmSalaryMonthRecordDO monthRecord) { + if (monthRecord == null) { + return null; + } + return BeanUtils.toBean(monthRecord, HrmSalaryMonthRecordRespVO.class); + } + + private void fillPayrollReadinessEmployeeDeptName( + List employees) { + if (CollUtil.isEmpty(employees)) { + return; + } + Map deptMap = deptApi.getDeptMap( + convertSet(employees, HrmSalaryPayrollReadinessRespVO.Employee::getDeptId)); + employees.forEach(employee -> MapUtils.findAndThen(deptMap, employee.getDeptId(), + dept -> employee.setDeptName(dept.getName()))); + } + + // ==================== Excel 导入导出 ==================== + + private List getPayrollImportEmployeeList(Long monthRecordId, boolean salaryTaxOnly) { + // 1. 获得指定或最近月度工资表 + HrmSalaryMonthRecordDO monthRecord = monthRecordId == null + ? salaryMonthRecordService.getLastMonthRecord() + : salaryMonthRecordService.getMonthRecord(monthRecordId); + if (monthRecord == null) { + return Collections.emptyList(); + } + + // 2. 获得指定工资表已有员工,或尚未核算时的计薪员工 + List employees; + if (monthRecordId != null) { + List employeeRecords = + monthEmployeeRecordService.getMonthEmployeeRecordListByMonthRecordId(monthRecordId); + if (CollUtil.isNotEmpty(employeeRecords)) { + Map employeeMap = employeeService.getEmployeeMap( + convertSet(employeeRecords, HrmSalaryMonthEmployeeRecordDO::getEmployeeId)); + employees = convertList(employeeRecords, record -> employeeMap.get(record.getEmployeeId()), + Objects::nonNull); + } else { + employees = salaryMonthRecordService.getPayrollEmployeeList(monthRecord); + } + } else { + employees = salaryMonthRecordService.getPayrollEmployeeList(monthRecord); + } + + // 3. 筛选已配置薪资组的员工 + Map salaryGroupMap = salaryGroupService.getEmployeeSalaryGroupMap(employees); + if (!salaryTaxOnly) { + return convertList(employees, employee -> employee, + employee -> salaryGroupMap.containsKey(employee.getId())); + } + + // 4. 个税累计模板只包含使用工资薪金计税规则的员工 + Set taxRuleIds = convertSet(salaryGroupMap.values(), HrmSalaryGroupDO::getTaxRuleId, + salaryGroup -> salaryGroup.getTaxRuleId() != null); + Map taxRuleMap = salaryTaxRuleService.getSalaryTaxRuleMap(taxRuleIds); + return convertList(employees, employee -> employee, employee -> { + HrmSalaryGroupDO salaryGroup = salaryGroupMap.get(employee.getId()); + HrmSalaryTaxRuleDO taxRule = salaryGroup == null ? null + : taxRuleMap.get(salaryGroup.getTaxRuleId()); + return taxRule != null && Objects.equals(taxRule.getType(), HrmSalaryTaxTypeEnum.SALARY.getType()); + }); + } + + private List> buildImportHead(List businessHeaders) { + List headers = Arrays.asList("员工名称", "岗位", "工号", "部门编号"); + return convertList(CollUtil.union(headers, businessHeaders), Collections::singletonList); + } + + private List> buildImportTemplateData(List employees, int blankColumnSize) { + return convertList(employees, employee -> { + List row = buildImportEmployeeRow(employee); + for (int i = 0; i < blankColumnSize; i++) { + row.add(""); + } + return row; + }); + } + + private List buildImportEmployeeRow(HrmEmployeeDO employee) { + return new ArrayList<>(Arrays.asList( + employee.getName(), employee.getPostName(), employee.getJobNumber(), employee.getDeptId())); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryOptionController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryOptionController.java new file mode 100644 index 0000000..ffd3ec8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryOptionController.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionSaveReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionUpdateEnabledReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionUpdateVisibleReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryOptionDO; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryOptionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - HRM 薪资项") +@RestController +@RequestMapping("/hrm/salary/option") +@Validated +public class HrmSalaryOptionController { + + @Resource + private HrmSalaryOptionService salaryOptionService; + + @PostMapping("/create") + @Operation(summary = "创建薪资项") + @PreAuthorize("@ss.hasPermission('hrm:salary:option:create')") + public CommonResult createSalaryOption(@Valid @RequestBody HrmSalaryOptionSaveReqVO reqVO) { + return success(salaryOptionService.createSalaryOption(reqVO)); + } + + @PutMapping("/update-enabled") + @Operation(summary = "更新薪资项启用状态") + @PreAuthorize("@ss.hasPermission('hrm:salary:option:update')") + public CommonResult updateSalaryOptionEnabled( + @Valid @RequestBody HrmSalaryOptionUpdateEnabledReqVO reqVO) { + salaryOptionService.updateSalaryOptionEnabled(reqVO.getId(), reqVO.getEnabled()); + return success(true); + } + + @PutMapping("/update-visible") + @Operation(summary = "更新薪资项显示状态") + @PreAuthorize("@ss.hasPermission('hrm:salary:option:update')") + public CommonResult updateSalaryOptionVisible( + @Valid @RequestBody HrmSalaryOptionUpdateVisibleReqVO reqVO) { + salaryOptionService.updateSalaryOptionVisible(reqVO.getId(), reqVO.getVisible()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除薪资项") + @Parameter(name = "id", description = "薪资项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:option:delete')") + public CommonResult deleteSalaryOption(@RequestParam("id") Long id) { + salaryOptionService.deleteSalaryOption(id); + return success(true); + } + + @PutMapping("/sync") + @Operation(summary = "同步标准薪资项") + @PreAuthorize("@ss.hasPermission('hrm:salary:option:update')") + public CommonResult syncSalaryOption() { + salaryOptionService.syncSalaryOption(); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得薪资项列表") + @PreAuthorize("@ss.hasPermission('hrm:salary:option:query')") + public CommonResult> getSalaryOptionList() { + List list = salaryOptionService.getSalaryOptionList(); + return success(BeanUtils.toBean(list, HrmSalaryOptionRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得薪资项精简列表", description = "只包含已启用的薪资项") + @Parameter(name = "adjustable", description = "是否可调薪", example = "true") + @PreAuthorize("@ss.hasPermission('hrm:salary:option:query')") + public CommonResult> getSalaryOptionSimpleList( + @RequestParam(value = "adjustable", required = false) Boolean adjustable) { + List list = salaryOptionService.getSalaryOptionList(adjustable); + return success(convertList(list, option -> new HrmSalaryOptionRespVO() + .setCode(option.getCode()).setParentCode(option.getParentCode()) + .setName(option.getName()).setCalculateEnabled(option.getCalculateEnabled()))); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipController.java new file mode 100644 index 0000000..7d9b03d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipController.java @@ -0,0 +1,135 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeeListReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.HrmSalarySlipOptionRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.HrmSalarySlipPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.HrmSalarySlipRemarkReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.HrmSalarySlipRespVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.slip.HrmSalarySlipDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.salary.slip.HrmSalarySlipService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 工资条") +@RestController +@RequestMapping("/hrm/salary/slip") +@Validated +public class HrmSalarySlipController { + + @Resource + private HrmSalarySlipService salarySlipService; + @Resource + private HrmEmployeeService employeeService; + + @Resource + private DeptApi deptApi; + + @GetMapping("/page") + @Operation(summary = "获得工资条分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:query')") + public CommonResult> getSalarySlipPage( + @Validated HrmSalarySlipPageReqVO reqVO) { + // 1. 根据员工展示条件获得员工编号 + if (StrUtil.isNotBlank(reqVO.getSearch()) || reqVO.getDeptId() != null) { + HrmEmployeeListReqVO employeeReqVO = new HrmEmployeeListReqVO(); + employeeReqVO.setSearch(reqVO.getSearch()); + employeeReqVO.setDeptId(reqVO.getDeptId()); + Set employeeIds = convertSet( + employeeService.getEmployeeList(employeeReqVO), HrmEmployeeDO::getId); + if (CollUtil.isNotEmpty(reqVO.getEmployeeIds())) { + employeeIds.retainAll(reqVO.getEmployeeIds()); + } + if (CollUtil.isEmpty(employeeIds)) { + return success(PageResult.empty()); + } + reqVO.setEmployeeIds(new ArrayList<>(employeeIds)); + } + + // 2. 查询工资条并拼接响应 + PageResult pageResult = salarySlipService.getSalarySlipPage(reqVO); + return success(new PageResult<>(buildSalarySlipRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/get") + @Operation(summary = "获得工资条") + @Parameter(name = "id", description = "工资条编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:query')") + public CommonResult getSalarySlip(@RequestParam("id") Long id) { + return success(buildSalarySlipRespVO(salarySlipService.getSalarySlip(id))); + } + + @PutMapping("/remark") + @Operation(summary = "修改工资条备注") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:update')") + public CommonResult updateSalarySlipRemark( + @Valid @RequestBody HrmSalarySlipRemarkReqVO reqVO) { + salarySlipService.updateSalarySlipRemark(reqVO); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private HrmSalarySlipRespVO buildSalarySlipRespVO(HrmSalarySlipDO salarySlip) { + if (salarySlip == null) { + return null; + } + return CollUtil.getFirst(buildSalarySlipRespVOList(Collections.singletonList(salarySlip))); + } + + private List buildSalarySlipRespVOList(List salarySlips) { + if (CollUtil.isEmpty(salarySlips)) { + return Collections.emptyList(); + } + // 1. 获取员工、部门信息 + Map employeeMap = employeeService.getEmployeeMap( + convertSet(salarySlips, HrmSalarySlipDO::getEmployeeId)); + Map deptMap = deptApi.getDeptMap( + convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId)); + + // 2. 拼接响应 + return convertList(salarySlips, salarySlip -> { + HrmSalarySlipRespVO respVO = BeanUtils.toBean(salarySlip, HrmSalarySlipRespVO.class); + HrmEmployeeDO employee = employeeMap.get(salarySlip.getEmployeeId()); + if (employee != null) { + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber()) + .setMobile(employee.getMobile()).setDeptId(employee.getDeptId()) + .setPostName(employee.getPostName()); + MapUtils.findAndThen(deptMap, employee.getDeptId(), dept -> respVO.setDeptName(dept.getName())); + } + respVO.setOptions(BeanUtils.toBean(salarySlip.getOptions(), HrmSalarySlipOptionRespVO.class)); + return respVO; + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipSendRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipSendRecordController.java new file mode 100644 index 0000000..40a57c9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipSendRecordController.java @@ -0,0 +1,182 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord.HrmSalarySlipSendEmployeeReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord.HrmSalarySlipSendEmployeeRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord.HrmSalarySlipSendRecordPageReqVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord.HrmSalarySlipSendRecordRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord.HrmSalarySlipSendReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.monthrecord.HrmSalaryMonthEmployeeRecordDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.slip.HrmSalarySlipSendRecordDO; +import cn.iocoder.yudao.module.hrm.service.employee.info.HrmEmployeeService; +import cn.iocoder.yudao.module.hrm.service.salary.slip.HrmSalarySlipService; +import cn.iocoder.yudao.module.hrm.service.salary.slip.HrmSalarySlipSendRecordService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 工资条发放记录") +@RestController +@RequestMapping("/hrm/salary/slip-send-record") +@Validated +public class HrmSalarySlipSendRecordController { + + @Resource + private HrmSalarySlipSendRecordService salarySlipSendRecordService; + @Resource + private HrmSalarySlipService salarySlipService; + @Resource + private HrmEmployeeService employeeService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @PostMapping("/create") + @Operation(summary = "发放工资条") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:create')") + public CommonResult sendSalarySlip(@Valid @RequestBody HrmSalarySlipSendReqVO reqVO) { + return success(salarySlipSendRecordService.sendSalarySlip(reqVO)); + } + + @GetMapping("/employee-page") + @Operation(summary = "获得工资条待发员工分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:query')") + public CommonResult> getSalarySlipSendEmployeePage( + @Valid HrmSalarySlipSendEmployeeReqVO reqVO) { + // 1. 获得员工月度工资分页 + PageResult pageResult = + salarySlipSendRecordService.getSalarySlipSendEmployeePage(reqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + + // 2.1 获取员工和发放状态 + Map employeeMap = employeeService.getEmployeeMap( + convertSet(pageResult.getList(), HrmSalaryMonthEmployeeRecordDO::getEmployeeId)); + Set sentMonthEmployeeRecordIds = salarySlipService.getSentMonthEmployeeRecordIdSet( + convertSet(pageResult.getList(), HrmSalaryMonthEmployeeRecordDO::getId)); + // 2.2 拼接响应 + return success(new PageResult<>(buildSalarySlipSendEmployeeRespVOList( + pageResult.getList(), employeeMap, sentMonthEmployeeRecordIds), pageResult.getTotal())); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工资条发放记录") + @Parameter(name = "id", description = "发放记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:delete')") + public CommonResult deleteSalarySlipSendRecord(@RequestParam("id") Long id) { + salarySlipSendRecordService.deleteSalarySlipSendRecord(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得工资条发放记录分页") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:query')") + public CommonResult> getSalarySlipSendRecordPage( + @Validated HrmSalarySlipSendRecordPageReqVO reqVO) { + PageResult pageResult = + salarySlipSendRecordService.getSalarySlipSendRecordPage(reqVO); + return success(new PageResult<>(buildSalarySlipSendRecordRespVOList(pageResult.getList()), + pageResult.getTotal())); + } + + @GetMapping("/get") + @Operation(summary = "获得工资条发放记录") + @Parameter(name = "id", description = "发放记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:query')") + public CommonResult getSalarySlipSendRecord(@RequestParam("id") Long id) { + HrmSalarySlipSendRecordDO slipSendRecord = + salarySlipSendRecordService.getSalarySlipSendRecord(id); + if (slipSendRecord == null) { + return success(null); + } + return success(CollUtil.getFirst(buildSalarySlipSendRecordRespVOList( + Collections.singletonList(slipSendRecord)))); + } + + // ==================== 拼接 VO ==================== + + private List buildSalarySlipSendRecordRespVOList( + List slipSendRecords) { + if (CollUtil.isEmpty(slipSendRecords)) { + return Collections.emptyList(); + } + // 1. 获取关联数据 + Map readCountMap = salarySlipService.getSalarySlipReadCountMap( + convertSet(slipSendRecords, HrmSalarySlipSendRecordDO::getId)); + Map creatorMap = adminUserApi.getUserMap( + convertSet(slipSendRecords, record -> NumberUtil.isNumber(record.getCreator()) + ? NumberUtils.parseLong(record.getCreator()) : null)); + + // 2. 拼接响应 + return BeanUtils.toBean(slipSendRecords, HrmSalarySlipSendRecordRespVO.class, record -> { + record.setReadCount(readCountMap.getOrDefault(record.getId(), 0L).intValue()) + .setCreatorName(record.getCreator()); + MapUtils.findAndThen(creatorMap, NumberUtil.isNumber(record.getCreator()) + ? NumberUtils.parseLong(record.getCreator()) : null, + creator -> record.setCreatorName(creator.getNickname())); + }); + } + + private List buildSalarySlipSendEmployeeRespVOList( + List employeeRecords, + Map employeeMap, Set sentMonthEmployeeRecordIds) { + if (CollUtil.isEmpty(employeeRecords)) { + return Collections.emptyList(); + } + // 1. 获取部门信息 + Set deptIds = convertSet(employeeMap.values(), HrmEmployeeDO::getDeptId); + Map deptMap = CollUtil.isEmpty(deptIds) + ? Collections.emptyMap() : deptApi.getDeptMap(deptIds); + + // 2. 拼接响应 + return convertList(employeeRecords, employeeRecord -> { + HrmSalarySlipSendEmployeeRespVO respVO = + BeanUtils.toBean(employeeRecord, HrmSalarySlipSendEmployeeRespVO.class); + respVO.setMonthEmployeeRecordId(employeeRecord.getId()); + respVO.setSent(sentMonthEmployeeRecordIds.contains(employeeRecord.getId())); + MapUtils.findAndThen(employeeMap, employeeRecord.getEmployeeId(), employee -> { + respVO.setEmployeeName(employee.getName()).setJobNumber(employee.getJobNumber()) + .setMobile(employee.getMobile()).setDeptId(employee.getDeptId()) + .setPostName(employee.getPostName()); + MapUtils.findAndThen(deptMap, employee.getDeptId(), + dept -> respVO.setDeptName(dept.getName())); + }); + return respVO; + }); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipTemplateController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipTemplateController.java new file mode 100644 index 0000000..e7dba6a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalarySlipTemplateController.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryOptionDO; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.template.HrmSalarySlipTemplateOptionVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.template.HrmSalarySlipTemplateRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.template.HrmSalarySlipTemplateSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.slip.HrmSalarySlipTemplateDO; +import cn.iocoder.yudao.module.hrm.service.salary.slip.HrmSalarySlipTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryOptionDO.ROOT_PARENT_CODE; + +@Tag(name = "管理后台 - HRM 工资条模板") +@RestController +@RequestMapping("/hrm/salary/slip-template") +@Validated +public class HrmSalarySlipTemplateController { + + @Resource + private HrmSalarySlipTemplateService salarySlipTemplateService; + + @PostMapping("/create") + @Operation(summary = "创建工资条模板") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:update')") + public CommonResult createSalarySlipTemplate( + @Valid @RequestBody HrmSalarySlipTemplateSaveReqVO createReqVO) { + return success(salarySlipTemplateService.createSalarySlipTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新工资条模板") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:update')") + public CommonResult updateSalarySlipTemplate( + @Valid @RequestBody HrmSalarySlipTemplateSaveReqVO updateReqVO) { + salarySlipTemplateService.updateSalarySlipTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工资条模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:delete')") + public CommonResult deleteSalarySlipTemplate(@RequestParam("id") Long id) { + salarySlipTemplateService.deleteSalarySlipTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得工资条模板") + @Parameter(name = "id", description = "工资条模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:query')") + public CommonResult getSalarySlipTemplate(@RequestParam("id") Long id) { + return success(buildSalarySlipTemplateRespVO( + salarySlipTemplateService.getSalarySlipTemplate(id))); + } + + @GetMapping("/list") + @Operation(summary = "获得工资条模板列表") + @PreAuthorize("@ss.hasPermission('hrm:salary:slip:query')") + public CommonResult> getSalarySlipTemplateList() { + return success(convertList(salarySlipTemplateService.getSalarySlipTemplateList(), + this::buildSalarySlipTemplateRespVO)); + } + + // ==================== 拼接 VO ==================== + + private HrmSalarySlipTemplateRespVO buildSalarySlipTemplateRespVO(HrmSalarySlipTemplateDO template) { + if (template == null) { + return null; + } + return BeanUtils.toBean(template, HrmSalarySlipTemplateRespVO.class) + .setOptions(buildSalarySlipTemplateOptionRespVOList(template.getOptions())); + } + + private List buildSalarySlipTemplateOptionRespVOList( + List options) { + List result = new ArrayList<>(convertList(options, + option -> BeanUtils.toBean(option, HrmSalarySlipTemplateOptionVO.class) + .setParentCode(ROOT_PARENT_CODE))); + for (HrmSalarySlipTemplateDO.Option option : options) { + result.addAll(convertList(option.getChildren(), child -> + BeanUtils.toBean(child, HrmSalarySlipTemplateOptionVO.class) + .setParentCode(option.getCode()))); + } + return result; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryTaxRuleController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryTaxRuleController.java new file mode 100644 index 0000000..cb2ff66 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryTaxRuleController.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.taxrule.HrmSalaryTaxRuleRespVO; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.taxrule.HrmSalaryTaxRuleSaveReqVO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config.HrmSalaryTaxRuleDO; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryGroupService; +import cn.iocoder.yudao.module.hrm.service.salary.config.HrmSalaryTaxRuleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - HRM 计税规则") +@RestController +@RequestMapping("/hrm/salary/tax-rule") +@Validated +public class HrmSalaryTaxRuleController { + + @Resource + private HrmSalaryTaxRuleService salaryTaxRuleService; + @Resource + private HrmSalaryGroupService salaryGroupService; + + @PostMapping("/create") + @Operation(summary = "创建计税规则") + @PreAuthorize("@ss.hasPermission('hrm:salary:tax-rule:create')") + public CommonResult createSalaryTaxRule(@Valid @RequestBody HrmSalaryTaxRuleSaveReqVO reqVO) { + return success(salaryTaxRuleService.createSalaryTaxRule(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改计税规则") + @PreAuthorize("@ss.hasPermission('hrm:salary:tax-rule:update')") + public CommonResult updateSalaryTaxRule(@Valid @RequestBody HrmSalaryTaxRuleSaveReqVO reqVO) { + salaryTaxRuleService.updateSalaryTaxRule(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除计税规则") + @Parameter(name = "id", description = "计税规则编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:tax-rule:delete')") + public CommonResult deleteSalaryTaxRule(@RequestParam("id") Long id) { + salaryTaxRuleService.deleteSalaryTaxRule(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得计税规则") + @Parameter(name = "id", description = "计税规则编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('hrm:salary:tax-rule:query')") + public CommonResult getSalaryTaxRule(@RequestParam("id") Long id) { + HrmSalaryTaxRuleDO taxRule = salaryTaxRuleService.getSalaryTaxRule(id); + return success(BeanUtils.toBean(taxRule, HrmSalaryTaxRuleRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得计税规则列表") + @PreAuthorize("@ss.hasPermission('hrm:salary:tax-rule:query')") + public CommonResult> getSalaryTaxRuleList() { + // 1. 获得计税规则及其使用数量 + List taxRules = salaryTaxRuleService.getSalaryTaxRuleList(); + Map usedGroupCountMap = salaryGroupService.getSalaryGroupCountMapByTaxRuleIds( + convertSet(taxRules, HrmSalaryTaxRuleDO::getId)); + + // 2. 拼接响应 + return success(BeanUtils.toBean(taxRules, HrmSalaryTaxRuleRespVO.class, + rule -> rule.setUsedGroupCount(usedGroupCountMap.getOrDefault(rule.getId(), 0L)))); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changerecord/HrmSalaryChangeRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changerecord/HrmSalaryChangeRecordRespVO.java new file mode 100644 index 0000000..c02b1f5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changerecord/HrmSalaryChangeRecordRespVO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changerecord; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 定薪/调薪记录 Response VO") +@Data +@ExcelIgnoreUnannotated +public class HrmSalaryChangeRecordRespVO { + + @ExcelProperty(value = "记录编号", index = 0) + @Schema(description = "记录编号") + private Long id; + + @ExcelProperty(value = "员工编号", index = 1) + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "记录类型") + private Integer recordType; + + @ExcelProperty(value = "记录类型", index = 2) + @Schema(description = "记录类型") + private String recordTypeName; + + @ExcelProperty(value = "调整原因", index = 3, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_SALARY_CHANGE_REASON) + @Schema(description = "调整原因") + private Integer changeReason; + + @ExcelProperty(value = "生效日期", index = 4) + @Schema(description = "生效日期") + private LocalDateTime effectTime; + + @ExcelProperty(value = "调整前正式工资", index = 5) + @Schema(description = "调整前正式工资") + private BigDecimal beforeTotal; + + @ExcelProperty(value = "调整后正式工资", index = 6) + @Schema(description = "调整后正式工资") + private BigDecimal afterTotal; + + @ExcelProperty(value = "调整前试用期工资", index = 7) + @Schema(description = "调整前试用期工资") + private BigDecimal probationBeforeTotal; + + @ExcelProperty(value = "调整后试用期工资", index = 8) + @Schema(description = "调整后试用期工资") + private BigDecimal probationAfterTotal; + + @ExcelProperty(value = "状态", index = 9, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_SALARY_CHANGE_RECORD_STATUS) + @Schema(description = "状态") + private Integer status; + + @ExcelProperty(value = "备注", index = 10) + @Schema(description = "备注") + private String remark; + + @Schema(description = "薪资项列表") + private List salaryOptions; + + @Schema(description = "试用期薪资项列表") + private List probationSalaryOptions; + + @ExcelProperty(value = "创建时间", index = 11) + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeOptionVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeOptionVO.java new file mode 100644 index 0000000..175d310 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeOptionVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changetemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 调薪项 VO") +@Data +public class HrmSalaryChangeOptionVO { + + @Schema(description = "薪资项名称", example = "基本工资") + private String name; + + @Schema(description = "薪资项编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "10101") + @NotNull(message = "薪资项编码不能为空") + private Integer code; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeTemplateRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeTemplateRespVO.java new file mode 100644 index 0000000..7be2224 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeTemplateRespVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changetemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 调薪模板 Response VO") +@Data +public class HrmSalaryChangeTemplateRespVO { + + @Schema(description = "调薪模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发调薪模板") + private String name; + + @Schema(description = "是否默认模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean defaultStatus; + + @Schema(description = "调薪项配置") + private List options; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeTemplateSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeTemplateSaveReqVO.java new file mode 100644 index 0000000..696c1b7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changetemplate/HrmSalaryChangeTemplateSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changetemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 调薪模板新增/修改 Request VO") +@Data +public class HrmSalaryChangeTemplateSaveReqVO { + + @Schema(description = "调薪模板编号", example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发调薪模板") + @NotBlank(message = "模板名称不能为空") + @Size(max = 64, message = "模板名称不能超过 64 个字符") + private String name; + + @Schema(description = "是否默认模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否默认模板不能为空") + private Boolean defaultStatus; + + @Schema(description = "调薪项配置", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "调薪项配置不能为空") + private List options; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigCreateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigCreateReqVO.java new file mode 100644 index 0000000..c532123 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigCreateReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.config.HrmSalarySocialSecurityMonthTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 计薪配置创建 Request VO") +@Data +public class HrmSalaryConfigCreateReqVO { + + @Schema(description = "计薪周期开始日", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "计薪周期开始日不能为空") + @Min(value = 1, message = "计薪周期开始日必须在 1 到 31 之间") + @Max(value = 31, message = "计薪周期开始日必须在 1 到 31 之间") + private Integer cycleStartDay; + + @Schema(description = "社保对应月份类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "社保对应月份类型不能为空") + @InEnum(value = HrmSalarySocialSecurityMonthTypeEnum.class, message = "社保对应月份类型必须是 {value}") + private Integer socialSecurityMonthType; + + @Schema(description = "工资开始年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + @NotNull(message = "工资开始年份不能为空") + @Min(value = 2000, message = "工资开始年份不能小于 2000") + @Max(value = 2100, message = "工资开始年份不能大于 2100") + private Integer startYear; + + @Schema(description = "工资开始月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + @NotNull(message = "工资开始月份不能为空") + @Min(value = 1, message = "工资开始月份必须在 1 到 12 之间") + @Max(value = 12, message = "工资开始月份必须在 1 到 12 之间") + private Integer startMonth; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigRespVO.java new file mode 100644 index 0000000..33a329a --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 计薪配置 Response VO") +@Data +public class HrmSalaryConfigRespVO { + + @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "计薪周期开始日", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer cycleStartDay; + + @Schema(description = "计薪周期结束日", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") + private Integer cycleEndDay; + + @Schema(description = "社保对应月份类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer socialSecurityMonthType; + + @Schema(description = "工资开始年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + private Integer startYear; + + @Schema(description = "工资开始月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + private Integer startMonth; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigUpdateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigUpdateReqVO.java new file mode 100644 index 0000000..b6c181b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/config/HrmSalaryConfigUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.config.HrmSalarySocialSecurityMonthTypeEnum; +import cn.iocoder.yudao.module.hrm.framework.operatelog.core.HrmSalarySocialSecurityMonthTypeParseFunction; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 计薪配置更新 Request VO") +@Data +public class HrmSalaryConfigUpdateReqVO { + + @Schema(description = "社保对应月份类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "社保对应月份类型不能为空") + @InEnum(value = HrmSalarySocialSecurityMonthTypeEnum.class, message = "社保对应月份类型必须是 {value}") + @DiffLogField(name = "社保对应月份", function = HrmSalarySocialSecurityMonthTypeParseFunction.NAME) + private Integer socialSecurityMonthType; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoImportRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoImportRespVO.java new file mode 100644 index 0000000..60b9c7b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoImportRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - HRM 员工薪资信息导入 Response VO") +@Data +@Builder +public class HrmSalaryEmployeeInfoImportRespVO { + + @Schema(description = "导入成功的工号数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List successJobNumbers; + + @Schema(description = "导入失败的员工集合,key 为工号或行号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureJobNumbers; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoPageReqVO.java new file mode 100644 index 0000000..2601ef5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoPageReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工薪资信息分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalaryEmployeeInfoPageReqVO extends PageParam { + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "员工编号列表", example = "[1024, 1025]") + private List employeeIds; + + @Schema(description = "调整类型", example = "1") + private Integer changeType; + + @Schema(description = "员工姓名或工号", example = "张三") + private String search; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "岗位名称", example = "Java 工程师") + private String postName; + + @Schema(description = "员工状态", example = "1") + private Integer status; + + @Schema(description = "入职状态", example = "1") + private Integer entryStatus; + + @Schema(description = "员工状态页签", example = "1") + private Integer statusCategory; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoRespVO.java new file mode 100644 index 0000000..c1969c0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoRespVO.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工薪资信息 Response VO") +@Data +@ExcelIgnoreUnannotated +public class HrmSalaryEmployeeInfoRespVO { + + @ExcelProperty(value = "员工薪资编号", index = 0) + @Schema(description = "员工薪资编号") + private Long id; + + @ExcelProperty(value = "员工编号", index = 1) + @Schema(description = "员工编号") + private Long employeeId; + + @ExcelProperty(value = "员工姓名", index = 2) + @Schema(description = "员工姓名") + private String employeeName; + + @ExcelProperty(value = "工号", index = 3) + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "手机号") + private String mobile; + + @ExcelProperty(value = "部门编号", index = 4) + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "岗位名称") + private String postName; + + @Schema(description = "入职状态") + private Integer entryStatus; + + @Schema(description = "员工状态") + private Integer status; + + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + @Schema(description = "转正时间") + private LocalDateTime regularTime; + + @ExcelProperty(value = "调整原因", index = 6, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_SALARY_CHANGE_REASON) + @Schema(description = "调整原因") + private Integer changeReason; + + @ExcelProperty(value = "生效日期", index = 7) + @Schema(description = "生效日期") + private LocalDateTime effectTime; + + @ExcelProperty(value = "薪资状态", index = 5, converter = DictConvert.class) + @DictFormat(DictTypeConstants.HRM_SALARY_CHANGE_TYPE) + @Schema(description = "薪资状态") + private Integer changeType; + + @ExcelProperty(value = "试用期工资", index = 9) + @Schema(description = "试用期工资") + private BigDecimal probationSalary; + + @ExcelProperty(value = "正式工资", index = 8) + @Schema(description = "正式工资") + private BigDecimal regularSalary; + + @ExcelProperty(value = "备注", index = 10) + @Schema(description = "备注") + private String remark; + + @Schema(description = "薪资项列表") + private List salaryOptions; + + @Schema(description = "试用期薪资项列表") + private List probationSalaryOptions; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateListReqVO.java new file mode 100644 index 0000000..a114d72 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateListReqVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryBatchAdjustTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryChangeReasonEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 批量调薪 Request VO") +@Data +public class HrmSalaryEmployeeInfoUpdateListReqVO { + + @Schema(description = "员工编号列表", example = "[1024, 1025]") + private List<@NotNull(message = "员工不能为空") Long> employeeIds; + + @Schema(description = "部门编号列表", example = "[100, 101]") + private List<@NotNull(message = "部门不能为空") Long> deptIds; + + @Schema(description = "调薪方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "调薪方式不能为空") + @InEnum(value = HrmSalaryBatchAdjustTypeEnum.class, message = "调薪方式必须是 {value}") + private Integer type; + + @Schema(description = "调整原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "调薪原因不能为空") + @InEnum(value = HrmSalaryChangeReasonEnum.class, message = "调薪原因必须是 {value}") + private Integer changeReason; + + @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "调薪生效日期不能为空") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "年度统一调薪") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @Schema(description = "薪资项列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "调薪项不能为空") + @Valid + private List salaryOptions; + + @AssertTrue(message = "至少需要选择一个部门或员工") + @JsonIgnore + public boolean isScopeValid() { + return CollUtil.isNotEmpty(deptIds) || CollUtil.isNotEmpty(employeeIds); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateListRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateListRespVO.java new file mode 100644 index 0000000..4f8e576 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateListRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - HRM 批量调薪 Response VO") +@Data +public class HrmSalaryEmployeeInfoUpdateListRespVO { + + @Schema(description = "成功员工编号列表") + private List successEmployeeIds = new ArrayList<>(); + + @Schema(description = "失败员工及原因") + private Map failureEmployeeReasons = new LinkedHashMap<>(); + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateReqVO.java new file mode 100644 index 0000000..20bd1a0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoUpdateReqVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryChangeReasonEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryChangeRecordTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工薪资信息修改 Request VO") +@Data +public class HrmSalaryEmployeeInfoUpdateReqVO { + + @Schema(description = "定薪/调薪记录编号;编辑待生效或已取消记录时传入", example = "1024") + private Long id; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工不能为空") + private Long employeeId; + + @Schema(description = "记录类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "记录类型不能为空") + @InEnum(value = HrmSalaryChangeRecordTypeEnum.class, message = "记录类型必须是 {value}") + private Integer recordType; + + @Schema(description = "调整原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "调整原因不能为空") + @InEnum(value = HrmSalaryChangeReasonEnum.class, message = "调整原因必须是 {value}") + private Integer changeReason; + + @Schema(description = "生效日期") + private LocalDateTime effectTime; + + @Schema(description = "备注", example = "年度调薪") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + + @Schema(description = "薪资项列表") + @Valid + private List salaryOptions; + + @Schema(description = "试用期薪资项列表") + @Valid + private List probationSalaryOptions; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupPageReqVO.java new file mode 100644 index 0000000..92cba98 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupPageReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.group; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 薪资组分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalaryGroupPageReqVO extends PageParam { + + @Schema(description = "薪资组名称", example = "总部薪资组") + private String name; + + @Schema(description = "计税规则编号", example = "1024") + private Long taxRuleId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupRespVO.java new file mode 100644 index 0000000..f01bd56 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupRespVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 薪资组 Response VO") +@Data +public class HrmSalaryGroupRespVO { + + @Schema(description = "薪资组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "薪资组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "总部薪资组") + private String name; + + @Schema(description = "月计薪标准", example = "21.75") + private BigDecimal salaryStandard; + + @Schema(description = "转正、调薪月规则") + private String changeRule; + + @Schema(description = "计税规则编号", example = "1024") + private Long taxRuleId; + + @Schema(description = "计税规则名称", example = "工资薪金所得税") + private String taxRuleName; + + @Schema(description = "适用部门编号列表") + private List deptIds; + + @Schema(description = "适用部门名称列表") + private List deptNames; + + @Schema(description = "适用员工编号列表") + private List employeeIds; + + @Schema(description = "适用员工名称列表") + private List employeeNames; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupSaveReqVO.java new file mode 100644 index 0000000..b8711ae --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/group/HrmSalaryGroupSaveReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.group; + +import cn.hutool.core.collection.CollUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 薪资组保存 Request VO") +@Data +public class HrmSalaryGroupSaveReqVO { + + @Schema(description = "薪资组编号", example = "1024") + private Long id; + + @Schema(description = "薪资组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "总部薪资组") + @NotBlank(message = "薪资组名称不能为空") + @Size(max = 64, message = "薪资组名称不能超过 64 个字符") + private String name; + + @Schema(description = "计税规则编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "计税规则不能为空") + private Long taxRuleId; + + @Schema(description = "适用部门编号列表", example = "[100, 101]") + private List deptIds; + + @Schema(description = "适用员工编号列表", example = "[1024, 1025]") + private List employeeIds; + + @AssertTrue(message = "适用部门和适用员工不能同时为空") + @JsonIgnore + public boolean isScopeValid() { + return CollUtil.isNotEmpty(deptIds) || CollUtil.isNotEmpty(employeeIds); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordCreateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordCreateReqVO.java new file mode 100644 index 0000000..d15e4c3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordCreateReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 月度工资表创建 Request VO") +@Data +public class HrmSalaryMonthRecordCreateReqVO { + + @Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + @NotNull(message = "年份不能为空") + @Min(value = 2000, message = "年份不能小于 2000") + @Max(value = 2100, message = "年份不能大于 2100") + private Integer year; + + @Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + @NotNull(message = "月份不能为空") + @Min(value = 1, message = "月份必须在 1 到 12 之间") + @Max(value = 12, message = "月份必须在 1 到 12 之间") + private Integer month; + + @Schema(description = "标题", example = "2026 年 7 月工资表") + @Size(max = 128, message = "标题不能超过 128 个字符") + private String title; + + @Schema(description = "开始日期") + private LocalDateTime startTime; + + @Schema(description = "结束日期") + private LocalDateTime endTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordPageReqVO.java new file mode 100644 index 0000000..1e6eb7e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.monthrecord.HrmSalaryMonthRecordStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 月度工资表分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalaryMonthRecordPageReqVO extends PageParam { + + @Schema(description = "年份", example = "2026") + private Integer year; + + @Schema(description = "月份", example = "7") + private Integer month; + + @Schema(description = "核算状态", example = "1") + @InEnum(value = HrmSalaryMonthRecordStatusEnum.class, message = "月度工资表状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordRespVO.java new file mode 100644 index 0000000..5a8a6f5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryMonthRecordRespVO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord; + +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionNodeRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 月度工资表 Response VO") +@Data +public class HrmSalaryMonthRecordRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + private Integer year; + + @Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + private Integer month; + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026 年 7 月工资表") + private String title; + + @Schema(description = "开始日期") + private LocalDateTime startTime; + + @Schema(description = "结束日期") + private LocalDateTime endTime; + + @Schema(description = "员工数量") + private Integer employeeCount; + + @Schema(description = "应发工资") + private BigDecimal expectedPaySalary; + + @Schema(description = "个人社保金额") + private BigDecimal personalInsuranceAmount; + + @Schema(description = "个人公积金金额") + private BigDecimal personalProvidentFundAmount; + + @Schema(description = "个人所得税") + private BigDecimal personalTax; + + @Schema(description = "实发工资") + private BigDecimal realPaySalary; + + @Schema(description = "公司社保金额") + private BigDecimal corporateInsuranceAmount; + + @Schema(description = "公司公积金金额") + private BigDecimal corporateProvidentFundAmount; + + @Schema(description = "核算状态") + private Integer status; + + @Schema(description = "薪资项表头") + private List optionHeaders; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryPayrollReadinessRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryPayrollReadinessRespVO.java new file mode 100644 index 0000000..6a844df --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/HrmSalaryPayrollReadinessRespVO.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - HRM 薪资核算准备 Response VO") +@Data +public class HrmSalaryPayrollReadinessRespVO { + + @Schema(description = "工资表编号") + private Long monthRecordId; + + @Schema(description = "标题") + private String title; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "开始日期") + private LocalDateTime startTime; + + @Schema(description = "结束日期") + private LocalDateTime endTime; + + @Schema(description = "社保年月") + private String socialSecurityYearMonth; + + @Schema(description = "计薪员工人数") + private Long payrollEmployeeCount; + + @Schema(description = "已定薪员工人数") + private Long salaryEmployeeCount; + + @Schema(description = "未定薪员工人数") + private Long noSalaryEmployeeCount; + + @Schema(description = "未分配薪资组员工人数") + private Long noSalaryGroupEmployeeCount; + + @Schema(description = "异动员工人数") + private Long changeEmployeeCount; + + @Schema(description = "变更类型数量映射") + private Map changeTypeCountMap; + + @Schema(description = "未定薪员工列表") + private List noSalaryEmployees; + @Schema(description = "未分配薪资组员工列表") + private List noSalaryGroupEmployees; + + @Schema(description = "管理后台 - HRM 薪资核算准备员工 Response VO") + @Data + public static class Employee { + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "员工姓名") + private String employeeName; + + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "职位名称") + private String postName; + + @Schema(description = "入职状态") + private Integer entryStatus; + + @Schema(description = "员工状态") + private Integer status; + + @Schema(description = "入职时间") + private LocalDateTime entryTime; + + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryEmployeeMonthRecordPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryEmployeeMonthRecordPageReqVO.java new file mode 100644 index 0000000..b7b9773 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryEmployeeMonthRecordPageReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.monthrecord.HrmSalaryMonthRecordStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 员工月度工资分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalaryEmployeeMonthRecordPageReqVO extends PageParam { + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工编号不能为空") + private Long employeeId; + + @Schema(description = "月度工资表状态", example = "10") + @InEnum(value = HrmSalaryMonthRecordStatusEnum.class, message = "月度工资表状态必须是 {value}") + private Integer monthRecordStatus; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordListReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordListReqVO.java new file mode 100644 index 0000000..6be8ad2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordListReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryEmployeeChangeTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Collection; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工工资列表 Request VO") +@Data +public class HrmSalaryMonthEmployeeRecordListReqVO { + + @Schema(description = "工资表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工资表编号不能为空") + private Long monthRecordId; + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "员工姓名", example = "张三") + private String employeeName; + + @Schema(description = "工号", example = "HRM001") + private String jobNumber; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "员工异动类型", example = "1") + @InEnum(value = HrmSalaryEmployeeChangeTypeEnum.class, message = "员工异动分类必须是 {value}") + private Integer employeeChangeType; + + @Schema(hidden = true) + private Collection employeeIds; + + @Schema(hidden = true) + private Boolean salarySlipSent; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordPageReqVO.java new file mode 100644 index 0000000..65bf102 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordPageReqVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.employee.HrmSalaryEmployeeChangeTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工工资表分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalaryMonthEmployeeRecordPageReqVO extends PageParam { + + @Schema(description = "工资表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工资表编号不能为空") + private Long monthRecordId; + + @Schema(description = "员工编号", example = "1024") + private Long employeeId; + + @Schema(description = "员工姓名", example = "张三") + private String employeeName; + + @Schema(description = "工号", example = "HRM001") + private String jobNumber; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "员工异动类型", example = "1") + @InEnum(value = HrmSalaryEmployeeChangeTypeEnum.class, message = "员工异动分类必须是 {value}") + private Integer employeeChangeType; + + @Schema(description = "员工编号列表", example = "[1024, 1025]") + private List employeeIds; + + @Schema(hidden = true) + private Boolean salarySlipSent; +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordRespVO.java new file mode 100644 index 0000000..578ac76 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordRespVO.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee; + +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工月度工资 Response VO") +@Data +public class HrmSalaryMonthEmployeeRecordRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "工资表编号") + private Long monthRecordId; + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "员工姓名") + private String employeeName; + + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门") + private String deptName; + + @Schema(description = "职位名称") + private String postName; + + @Schema(description = "计薪出勤天数") + private BigDecimal actualWorkDay; + + @Schema(description = "应出勤天数") + private BigDecimal needWorkDay; + + @Schema(description = "应发工资") + private BigDecimal expectedPaySalary; + + @Schema(description = "应税工资") + private BigDecimal taxableSalary; + + @Schema(description = "个人所得税") + private BigDecimal personalTax; + + @Schema(description = "实发工资") + private BigDecimal realPaySalary; + + @Schema(description = "绩效系数") + private BigDecimal performanceCoefficient; + + @Schema(description = "薪资项值列表") + private List optionValues; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordUpdateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordUpdateReqVO.java new file mode 100644 index 0000000..8c4a103 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryMonthEmployeeRecordUpdateReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee; + +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 员工工资项修改 Request VO") +@Data +public class HrmSalaryMonthEmployeeRecordUpdateReqVO { + + @Schema(description = "员工月度工资记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "员工工资记录编号不能为空") + private Long id; + + @Schema(description = "薪资项值列表") + @Valid + private List optionValues; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryPerformanceCoefficientReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryPerformanceCoefficientReqVO.java new file mode 100644 index 0000000..a49f75b --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/monthrecord/employee/HrmSalaryPerformanceCoefficientReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.monthrecord.employee; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 绩效系数查询 Request VO") +@Data +public class HrmSalaryPerformanceCoefficientReqVO { + + @Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026") + @NotNull(message = "年份不能为空") + private Integer year; + + @Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + @NotNull(message = "月份不能为空") + private Integer month; + + @Schema(description = "员工编号列表", example = "[1024, 1025]") + private List employeeIds; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionNodeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionNodeRespVO.java new file mode 100644 index 0000000..26bf3b4 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionNodeRespVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 工资表薪资项节点 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalaryOptionNodeRespVO extends HrmSalaryOptionRespVO { + + @Schema(description = "子薪资项") + private List children; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionRespVO.java new file mode 100644 index 0000000..4e821a2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionRespVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 工资表薪资项 Response VO") +@Data +public class HrmSalaryOptionRespVO { + + @Schema(description = "薪资项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "薪资项编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "10101") + private Integer code; + + @Schema(description = "父薪资项编码", example = "10") + private Integer parentCode; + + @Schema(description = "薪资项名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "基本工资") + private String name; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否系统默认项", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean systemFlag; + + @Schema(description = "薪资项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "是否计税", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean taxEnabled; + + @Schema(description = "是否显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean visible; + + @Schema(description = "是否参与计算", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean calculateEnabled; + + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean enabled; + + @Schema(description = "标准薪资项目录编号", example = "1024") + private Long templateId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionSaveReqVO.java new file mode 100644 index 0000000..a393e57 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionSaveReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 工资表薪资项保存 Request VO") +@Data +public class HrmSalaryOptionSaveReqVO { + + @Schema(description = "父薪资项编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "薪资项分类不能为空") + private Integer parentCode; + + @Schema(description = "薪资项名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "基本工资") + @NotBlank(message = "薪资项名称不能为空") + @Size(max = 64, message = "薪资项名称长度不能超过 64 个字符") + private String name; + + @Schema(description = "备注", example = "固定薪资项") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionUpdateEnabledReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionUpdateEnabledReqVO.java new file mode 100644 index 0000000..ecca90e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionUpdateEnabledReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 薪资项更新启用状态 Request VO") +@Data +public class HrmSalaryOptionUpdateEnabledReqVO { + + @Schema(description = "薪资项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "薪资项编号不能为空") + private Long id; + + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "启用状态不能为空") + private Boolean enabled; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionUpdateVisibleReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionUpdateVisibleReqVO.java new file mode 100644 index 0000000..e38b9e0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionUpdateVisibleReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 薪资项更新显示状态 Request VO") +@Data +public class HrmSalaryOptionUpdateVisibleReqVO { + + @Schema(description = "薪资项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "薪资项编号不能为空") + private Long id; + + @Schema(description = "是否显示", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "显示状态不能为空") + private Boolean visible; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionValueVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionValueVO.java new file mode 100644 index 0000000..d816ced --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/option/HrmSalaryOptionValueVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 薪资项值 VO") +@Data +public class HrmSalaryOptionValueVO { + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "10101") + @NotNull(message = "薪资项编码不能为空") + private Integer code; + + @Schema(description = "薪资选项值名称") + private String name; + + @Schema(description = "值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00") + @NotNull(message = "薪资项值不能为空") + @Digits(integer = 10, fraction = 2, message = "薪资项值最多 10 位整数和 2 位小数") + private BigDecimal value; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipOptionRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipOptionRespVO.java new file mode 100644 index 0000000..04b19f6 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipOptionRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - HRM 工资条项 Response VO") +@Data +public class HrmSalarySlipOptionRespVO { + + @Schema(description = "薪资工资条选项名称") + private String name; + + @Schema(description = "薪资工资条选项类型") + private Integer type; + + @Schema(description = "编码") + private Integer code; + + @Schema(description = "值") + private BigDecimal value; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "子工资条项") + private List children; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipPageReqVO.java new file mode 100644 index 0000000..b48d36c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipPageReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 工资条分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalarySlipPageReqVO extends PageParam { + @Schema(description = "工资条记录编号", example = "1024") + private Long sendRecordId; + @Schema(description = "员工编号", example = "2048") + private Long employeeId; + @Schema(description = "员工编号列表", example = "[2048, 2049]") + private List employeeIds; + @Schema(description = "搜索关键字", example = "张三") + private String search; + @Schema(description = "部门编号", example = "100") + private Long deptId; + @Schema(description = "已读状态", example = "1") + private Integer readStatus; + @Schema(description = "备注", example = "7 月工资条") + private String remark; +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipRemarkReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipRemarkReqVO.java new file mode 100644 index 0000000..2a4616e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipRemarkReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 工资条备注 Request VO") +@Data +public class HrmSalarySlipRemarkReqVO { + + @Schema(description = "工资条编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工资条编号不能为空") + private Long id; + + @Schema(description = "备注", example = "本月绩效奖金已计入") + @Size(max = 500, message = "备注不能超过 500 个字符") + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipRespVO.java new file mode 100644 index 0000000..86b98fb --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/HrmSalarySlipRespVO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 工资条 Response VO") +@Data +public class HrmSalarySlipRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "工资条记录编号") + private Long sendRecordId; + + @Schema(description = "员工工资记录编号") + private Long monthEmployeeRecordId; + + @Schema(description = "员工编号") + private Long employeeId; + + @Schema(description = "员工姓名") + private String employeeName; + + @Schema(description = "工号") + private String jobNumber; + + @Schema(description = "手机号码") + private String mobile; + + @Schema(description = "部门编号") + private Long deptId; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "岗位名称") + private String postName; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "已读状态") + private Integer readStatus; + + @Schema(description = "实际工资") + private BigDecimal realPaySalary; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "选项列表") + private List options; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendEmployeeReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendEmployeeReqVO.java new file mode 100644 index 0000000..199d48e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendEmployeeReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 工资条待发员工 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalarySlipSendEmployeeReqVO extends PageParam { + + @Schema(description = "月度工资表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "月度工资表编号不能为空") + private Long monthRecordId; + + @Schema(description = "员工姓名、工号或手机号", example = "张三") + private String search; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "是否已发送", example = "false") + private Boolean sent; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendEmployeeRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendEmployeeRespVO.java new file mode 100644 index 0000000..27359dd --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendEmployeeRespVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - HRM 工资条待发员工 Response VO") +@Data +public class HrmSalarySlipSendEmployeeRespVO { + + @Schema(description = "员工月度工资记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long monthEmployeeRecordId; + + @Schema(description = "员工编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long employeeId; + + @Schema(description = "员工姓名", example = "张三") + private String employeeName; + + @Schema(description = "工号", example = "HRM001") + private String jobNumber; + + @Schema(description = "手机号码", example = "13800138000") + private String mobile; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门名称", example = "研发部") + private String deptName; + + @Schema(description = "岗位名称", example = "Java 工程师") + private String postName; + + @Schema(description = "应发工资", example = "10000") + private BigDecimal expectedPaySalary; + + @Schema(description = "实发工资", example = "8500") + private BigDecimal realPaySalary; + + @Schema(description = "是否已发送", example = "false") + private Boolean sent; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendRecordPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendRecordPageReqVO.java new file mode 100644 index 0000000..c7ca7a2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendRecordPageReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - HRM 工资条发放记录分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HrmSalarySlipSendRecordPageReqVO extends PageParam { + + @Schema(description = "年份", example = "2026") + private Integer year; + + @Schema(description = "月份", example = "7") + private Integer month; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendRecordRespVO.java new file mode 100644 index 0000000..5741c81 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendRecordRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 工资条发放记录 Response VO") +@Data +public class HrmSalarySlipSendRecordRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "工资表编号") + private Long monthRecordId; + + @Schema(description = "计薪人数") + private Integer employeeCount; + + @Schema(description = "发薪人数") + private Integer sendEmployeeCount; + + @Schema(description = "已读人数") + private Integer readCount; + + @Schema(description = "年份") + private Integer year; + + @Schema(description = "月份") + private Integer month; + + @Schema(description = "创建人编号") + private String creator; + + @Schema(description = "创建人姓名") + private String creatorName; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendReqVO.java new file mode 100644 index 0000000..e4557f8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/sendrecord/HrmSalarySlipSendReqVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.sendrecord; + +import cn.hutool.core.collection.CollUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.template.HrmSalarySlipTemplateOptionVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 工资条发放 Request VO") +@Data +public class HrmSalarySlipSendReqVO { + + @Schema(description = "工资表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工资表编号不能为空") + private Long monthRecordId; + + @Schema(description = "是否隐藏空值项", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否隐藏空值项不能为空") + private Boolean hideEmpty; + + @Schema(description = "工资条模板项列表") + @Valid + private List options; + + @Schema(description = "是否发放全部筛选结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否发放全部筛选结果不能为空") + private Boolean all; + + @Schema(description = "员工编号列表", example = "[1024, 1025]") + private List employeeIds; + + @Schema(description = "员工姓名、工号或手机号", example = "张三") + private String search; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "是否已发送", example = "false") + private Boolean sent; + + @AssertTrue(message = "发放已选员工时,员工不能为空") + @JsonIgnore + public boolean isEmployeeIdsValid() { + return Boolean.TRUE.equals(all) || CollUtil.isNotEmpty(employeeIds); + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateOptionVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateOptionVO.java new file mode 100644 index 0000000..035b941 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateOptionVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - HRM 工资条模板项 VO") +@Data +public class HrmSalarySlipTemplateOptionVO { + + @Schema(description = "薪资工资条模板选项名称") + @NotBlank(message = "工资条模板项名称不能为空") + @Size(max = 64, message = "工资条模板项名称不能超过 64 个字符") + private String name; + + @Schema(description = "薪资工资条模板选项类型") + private Integer type; + + @Schema(description = "编码") + private Integer code; + + @Schema(description = "备注") + @Size(max = 255, message = "备注不能超过 255 个字符") + private String remark; + + @Schema(description = "父级编码") + private Integer parentCode; + + @Schema(description = "是否隐藏") + private Boolean hidden; + + @Schema(description = "排序") + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateRespVO.java new file mode 100644 index 0000000..deae0e7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - HRM 工资条模板 Response VO") +@Data +public class HrmSalarySlipTemplateRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "模板名称") + private String name; + + @Schema(description = "是否隐藏空值项") + private Boolean hideEmpty; + + @Schema(description = "是否默认选项") + private Boolean defaultStatus; + + @Schema(description = "选项列表") + private List options; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateSaveReqVO.java new file mode 100644 index 0000000..e0885a8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/slip/template/HrmSalarySlipTemplateSaveReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.slip.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - HRM 工资条模板新增/修改 Request VO") +@Data +public class HrmSalarySlipTemplateSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "标准工资条模板") + @NotBlank(message = "模板名称不能为空") + @Size(max = 64, message = "模板名称不能超过 64 个字符") + private String name; + + @Schema(description = "是否隐藏空值项", example = "true") + private Boolean hideEmpty; + + @Schema(description = "选项列表") + @Valid + private List options; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/taxrule/HrmSalaryTaxRuleRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/taxrule/HrmSalaryTaxRuleRespVO.java new file mode 100644 index 0000000..c93b6fe --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/taxrule/HrmSalaryTaxRuleRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.taxrule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - HRM 计税规则 Response VO") +@Data +public class HrmSalaryTaxRuleRespVO { + + @Schema(description = "计税规则编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "计税规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工资薪金所得税") + private String name; + + @Schema(description = "计税类型", example = "1") + private Integer type; + + @Schema(description = "是否计税", example = "true") + private Boolean taxEnabled; + + @Schema(description = "起征阈值", example = "5000") + private BigDecimal threshold; + + @Schema(description = "小数位数", example = "2") + private Integer decimalScale; + + @Schema(description = "计税周期类型", example = "1") + private Integer cycleType; + + @Schema(description = "使用该规则的薪资组数量", example = "2") + private Long usedGroupCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/taxrule/HrmSalaryTaxRuleSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/taxrule/HrmSalaryTaxRuleSaveReqVO.java new file mode 100644 index 0000000..63a6b6f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/taxrule/HrmSalaryTaxRuleSaveReqVO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.taxrule; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.config.HrmSalaryTaxCycleTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.salary.config.HrmSalaryTaxTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + + +@Schema(description = "管理后台 - HRM 计税规则保存 Request VO") +@Data +public class HrmSalaryTaxRuleSaveReqVO { + + @Schema(description = "计税规则编号", example = "1024") + private Long id; + + @Schema(description = "计税规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工资薪金所得税") + @NotBlank(message = "计税规则名称不能为空") + @Size(max = 64, message = "计税规则名称长度不能超过 64 个字符") + private String name; + + @Schema(description = "计税类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "计税类型不能为空") + @InEnum(value = HrmSalaryTaxTypeEnum.class, message = "计税类型必须是 {value}") + private Integer type; + + @Schema(description = "是否计税", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否计税不能为空") + private Boolean taxEnabled; + + @Schema(description = "起征阈值", example = "5000") + @DecimalMin(value = "0.00", message = "起征阈值不能小于 0") + @Digits(integer = 10, fraction = 2, message = "起征阈值最多 10 位整数和 2 位小数") + private BigDecimal threshold; + + @Schema(description = "小数位数", example = "2") + @Min(value = 0, message = "小数位数不能小于 0") + @Max(value = 4, message = "小数位数不能大于 4") + private Integer decimalScale; + + @Schema(description = "计税周期类型", example = "1") + @InEnum(value = HrmSalaryTaxCycleTypeEnum.class, message = "计税周期类型必须是 {value}") + private Integer cycleType; + + @AssertTrue(message = "计税规则的起征阈值和小数位数不能为空") + @JsonIgnore + public boolean isTaxConfigValid() { + return HrmSalaryTaxTypeEnum.NONE.getType().equals(type) || threshold != null && decimalScale != null; + } + + @AssertTrue(message = "工资薪金所得税的计税周期不能为空") + @JsonIgnore + public boolean isCycleTypeValid() { + return ObjUtil.notEqual(HrmSalaryTaxTypeEnum.SALARY.getType(), type) || cycleType != null; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/package-info.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/package-info.java new file mode 100644 index 0000000..320cae4 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供 RESTful API 给前端 + * + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. admin.portal 包:提供给 HRM 员工自助端 + */ +package cn.iocoder.yudao.module.hrm.controller; diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/config/HrmAttendanceGroupDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/config/HrmAttendanceGroupDO.java new file mode 100644 index 0000000..1f4273d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/config/HrmAttendanceGroupDO.java @@ -0,0 +1,290 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceAbsenteeismDeductMethodEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceLateEarlyDeductMethodEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceMisscardDeductMethodEnum; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +/** + * HRM 考勤组 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_attendance_group", autoResultMap = true) +@KeySequence("hrm_attendance_group_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmAttendanceGroupDO extends BaseDO { + + /** + * 考勤组编号 + */ + @TableId + private Long id; + /** + * 考勤组名称 + */ + private String name; + /** + * 适用部门编号 + * + * 关联 {@link DeptDO#getId()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List deptIds; + /** + * 适用员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List employeeIds; + /** + * 是否启用 WiFi 打卡 + */ + private Boolean openWifiCard; + /** + * 是否启用定位打卡 + */ + private Boolean openPointCard; + /** + * 班次配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List shifts; + /** + * 是否法定节假日休息 + */ + private Boolean rest; + /** + * 特殊日期设置数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List specialDates; + /** + * 打卡地点配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List points; + /** + * WiFi 打卡配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List wifis; + /** + * 扣款规则 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private DeductRule deductRule; + /** + * 是否默认考勤组 + */ + private Boolean defaultStatus; + + /** + * 特殊日期设置 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SpecialDate { + + /** + * 日期类型 + * + * 枚举 {@link HrmAttendanceHolidayTypeEnum} + */ + private Integer type; + /** + * 特殊日期 + */ + private LocalDateTime date; + } + + /** + * 考勤班次配置 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Shift { + + /** + * 工作日数组 + */ + private List weeks; + /** + * 上班时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime startTime; + /** + * 下班时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime endTime; + /** + * 上班打卡开始时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime clockInStartTime; + /** + * 上班打卡结束时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime clockInEndTime; + /** + * 下班打卡开始时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime clockOutStartTime; + /** + * 下班打卡结束时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime clockOutEndTime; + /** + * 休息开始时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime restStartTime; + /** + * 休息结束时间 + */ + @JsonFormat(pattern = "HH:mm") + private LocalTime restEndTime; + /** + * 休息时间是否不计入工作时长 + */ + private Boolean excludeRestTime; + } + + /** + * 打卡地点配置 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Point { + + /** + * 地点名称 + */ + private String name; + /** + * 定位地址 + */ + private String address; + /** + * 纬度 + */ + private BigDecimal latitude; + /** + * 经度 + */ + private BigDecimal longitude; + /** + * 有效打卡半径,单位:米 + */ + private Integer radius; + } + + /** + * WiFi 打卡配置 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Wifi { + + /** + * WiFi 名称 + */ + private String ssid; + /** + * MAC 地址 + */ + private String mac; + } + + /** + * 考勤扣款规则 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DeductRule { + + /** + * 迟到扣款方式 + * + * 枚举 {@link HrmAttendanceLateEarlyDeductMethodEnum} + */ + private Integer lateMethod; + /** + * 迟到扣款金额 + */ + private BigDecimal lateDeductMoney; + /** + * 早退扣款方式 + * + * 枚举 {@link HrmAttendanceLateEarlyDeductMethodEnum} + */ + private Integer earlyMethod; + /** + * 早退扣款金额 + */ + private BigDecimal earlyDeductMoney; + /** + * 旷工扣款方式 + * + * 枚举 {@link HrmAttendanceAbsenteeismDeductMethodEnum} + */ + private Integer absenteeismMethod; + /** + * 旷工扣款金额 + */ + private BigDecimal absenteeismDeductMoney; + /** + * 缺卡扣款方式 + * + * 枚举 {@link HrmAttendanceMisscardDeductMethodEnum} + */ + private Integer misscardMethod; + /** + * 缺卡扣款金额 + */ + private BigDecimal misscardDeductMoney; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/config/HrmAttendanceHolidayDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/config/HrmAttendanceHolidayDO.java new file mode 100644 index 0000000..662f0a3 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/config/HrmAttendanceHolidayDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.enums.attendance.config.HrmAttendanceHolidayTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 考勤节假日 DO + * + * @author 芋道源码 + */ +@TableName("hrm_attendance_holiday") +@KeySequence("hrm_attendance_holiday_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmAttendanceHolidayDO extends BaseDO { + + /** + * 节假日编号 + */ + @TableId + private Long id; + /** + * 日期 + */ + private LocalDateTime date; + /** + * 日期类型 + * + * 枚举 {@link HrmAttendanceHolidayTypeEnum} + * 字典 {@link DictTypeConstants#HRM_ATTENDANCE_HOLIDAY_TYPE} + */ + private Integer type; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/record/HrmAttendanceClockDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/record/HrmAttendanceClockDO.java new file mode 100644 index 0000000..aee355d --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/record/HrmAttendanceClockDO.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockSourceEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockStageEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.attendance.record.HrmAttendanceClockTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * HRM 考勤打卡记录 DO + * + * @author 芋道源码 + */ +@TableName("hrm_attendance_clock") +@KeySequence("hrm_attendance_clock_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmAttendanceClockDO extends BaseDO { + + /** + * 打卡记录编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 打卡时间 + */ + private LocalDateTime clockTime; + /** + * 打卡类型 + * + * 枚举 {@link HrmAttendanceClockTypeEnum} + * 字典 {@link DictTypeConstants#HRM_ATTENDANCE_CLOCK_TYPE} + */ + private Integer type; + /** + * 应打卡时间 + */ + private LocalDateTime attendanceTime; + /** + * 打卡来源 + * + * 枚举 {@link HrmAttendanceClockSourceEnum} + * 字典 {@link DictTypeConstants#HRM_ATTENDANCE_CLOCK_SOURCE} + */ + private Integer sourceType; + /** + * 打卡状态 + * + * 枚举 {@link HrmAttendanceClockStatusEnum} + * 字典 {@link DictTypeConstants#HRM_ATTENDANCE_CLOCK_STATUS} + */ + private Integer status; + /** + * 打卡阶段 + * + * 枚举 {@link HrmAttendanceClockStageEnum} + */ + private Integer stage; + /** + * 打卡地址 + */ + private String address; + /** + * 经度 + */ + private BigDecimal longitude; + /** + * 纬度 + */ + private BigDecimal latitude; + /** + * WiFi 名称 + */ + private String ssid; + /** + * WiFi MAC 地址 + */ + private String mac; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/record/HrmAttendanceLeaveDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/record/HrmAttendanceLeaveDO.java new file mode 100644 index 0000000..a1b0be8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/record/HrmAttendanceLeaveDO.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.record; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * HRM 考勤请假 DO + * + * @author 芋道源码 + */ +@TableName("hrm_attendance_leave") +@KeySequence("hrm_attendance_leave_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmAttendanceLeaveDO extends BaseDO { + + /** + * 请假记录编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 请假类型 + * + * 字典 {@link DictTypeConstants#HRM_ATTENDANCE_LEAVE_TYPE} + */ + private String type; + /** + * 请假开始时间 + */ + private LocalDateTime startTime; + /** + * 请假结束时间 + */ + private LocalDateTime endTime; + /** + * 请假时长 + */ + private BigDecimal day; + /** + * 请假理由 + */ + private String reason; + /** + * 备注 + */ + private String remark; + /** + * 审批状态 + * + * 枚举 {@link BpmProcessInstanceStatusEnum} + */ + private Integer approvalStatus; + /** + * BPM 流程实例编号 + * + * 关联 ProcessInstance 的 id 属性 + */ + private String processInstanceId; + /** + * 审批完成时间 + */ + private LocalDateTime approvalTime; + /** + * 审批或取消原因 + */ + private String approvalReason; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/config/HrmConfigDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/config/HrmConfigDO.java new file mode 100644 index 0000000..48977a8 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/config/HrmConfigDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.config.HrmConfigTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * HRM 通用业务配置 DO + * + * @author 芋道源码 + */ +@TableName("hrm_config") +@KeySequence("hrm_config_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmConfigDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 配置类型 + * + * 枚举 {@link HrmConfigTypeEnum} + */ + private Integer type; + /** + * 配置值 + */ + private String value; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeChangeRecordDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeChangeRecordDO.java new file mode 100644 index 0000000..69283eb --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeChangeRecordDO.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeReasonEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeChangeTypeEnum; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工异动记录 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_change_record") +@KeySequence("hrm_employee_change_record_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeChangeRecordDO extends BaseDO { + + /** + * 异动记录编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 异动类型 + * + * 枚举 {@link HrmEmployeeChangeTypeEnum} + */ + private Integer type; + /** + * 异动原因 + * + * 枚举 {@link HrmEmployeeChangeReasonEnum} + */ + private Integer reason; + /** + * 原部门编号 + * + * 关联 {@link DeptDO#getId()} + */ + private Long oldDeptId; + /** + * 新部门编号 + * + * 关联 {@link DeptDO#getId()} + */ + private Long newDeptId; + /** + * 原岗位 + */ + private String oldPostName; + /** + * 新岗位 + */ + private String newPostName; + /** + * 原职级 + */ + private String oldPostLevel; + /** + * 新职级 + */ + private String newPostLevel; + /** + * 原工作地点 + */ + private String oldWorkAddress; + /** + * 新工作地点 + */ + private String newWorkAddress; + /** + * 原直属上级员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long oldLeaderEmployeeId; + /** + * 新直属上级员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long newLeaderEmployeeId; + /** + * 试用期,单位:月 + */ + private Integer probation; + /** + * 生效日期 + */ + private LocalDateTime effectTime; + /** + * 实际生效时间 + */ + private LocalDateTime appliedTime; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeContractDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeContractDO.java new file mode 100644 index 0000000..0fdf476 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeContractDO.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeContractStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeContractTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * HRM 员工合同 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_employee_contract", autoResultMap = true) +@KeySequence("hrm_employee_contract_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeContractDO extends BaseDO { + + /** + * 合同编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 合同编号 + */ + private String no; + /** + * 合同类型 + * + * 枚举 {@link HrmEmployeeContractTypeEnum} + */ + private Integer type; + /** + * 合同开始日期 + */ + private LocalDateTime startTime; + /** + * 合同结束日期 + */ + private LocalDateTime endTime; + /** + * 合同期限,单位:年 + */ + private Integer term; + /** + * 合同状态 + * + * 枚举 {@link HrmEmployeeContractStatusEnum} + */ + private Integer status; + /** + * 签约公司 + */ + private String signCompany; + /** + * 合同签订日期 + */ + private LocalDateTime signTime; + /** + * 备注 + */ + private String remark; + /** + * 是否到期提醒 + * + */ + private Boolean expireRemind; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 附件地址数组 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List fileUrls; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeQuitInfoDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeQuitInfoDO.java new file mode 100644 index 0000000..24f40d9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeQuitInfoDO.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitReasonEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.employment.HrmEmployeeQuitTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工离职信息 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_quit_info") +@KeySequence("hrm_employee_quit_info_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeQuitInfoDO extends BaseDO { + + /** + * 离职信息编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 计划离职时间 + */ + private LocalDateTime planQuitTime; + /** + * 申请离职时间 + */ + private LocalDateTime applyQuitTime; + /** + * 薪资结算时间 + */ + private LocalDateTime salarySettlementTime; + /** + * 离职类型 + * + * 枚举 {@link HrmEmployeeQuitTypeEnum} + */ + private Integer type; + /** + * 离职原因 + * + * 枚举 {@link HrmEmployeeQuitReasonEnum} + */ + private Integer reason; + /** + * 备注 + */ + private String remark; + /** + * 离职前员工状态 + * + * 枚举 {@link HrmEmployeeStatusEnum} + */ + private Integer oldEmployeeStatus; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeSalaryCardDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeSalaryCardDO.java new file mode 100644 index 0000000..de9d2e5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/employment/HrmEmployeeSalaryCardDO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.ip.core.Area; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * HRM 员工工资卡 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_salary_card") +@KeySequence("hrm_employee_salary_card_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeSalaryCardDO extends BaseDO { + + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 银行卡号 + */ + private String bankCardNumber; + /** + * 开户地区编号 + * + * 关联 {@link Area#getId()} + */ + private Integer bankAreaId; + /** + * 银行名称 + */ + private String bankName; + /** + * 开户支行名称 + */ + private String bankBranchName; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeCertificateDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeCertificateDO.java new file mode 100644 index 0000000..f8334c5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeCertificateDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工证书 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_certificate") +@KeySequence("hrm_employee_certificate_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeCertificateDO extends BaseDO { + + /** + * 证书编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 证书名称 + */ + private String name; + /** + * 证书级别 + */ + private String level; + /** + * 证书编号 + */ + private String no; + /** + * 有效开始日期 + */ + private LocalDateTime startTime; + /** + * 有效结束日期 + */ + private LocalDateTime endTime; + /** + * 发证机构 + */ + private String issuingAuthority; + /** + * 发证日期 + */ + private LocalDateTime issuingTime; + /** + * 备注 + */ + private String remark; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeEducationExperienceDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeEducationExperienceDO.java new file mode 100644 index 0000000..c0d2de9 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeEducationExperienceDO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.enums.employee.experience.HrmEmployeeEducationEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.experience.HrmEmployeeTeachingMethodEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工教育经历 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_education_experience") +@KeySequence("hrm_employee_education_experience_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeEducationExperienceDO extends BaseDO { + + /** + * 教育经历编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 学历 + * + * 枚举 {@link HrmEmployeeEducationEnum} + * 字典 {@link DictTypeConstants#HRM_EMPLOYEE_EDUCATION} + */ + private Integer education; + /** + * 毕业院校 + */ + private String graduateSchool; + /** + * 专业 + */ + private String major; + /** + * 入学日期 + */ + private LocalDateTime admissionTime; + /** + * 毕业日期 + */ + private LocalDateTime graduationTime; + /** + * 教学方式 + * + * 枚举 {@link HrmEmployeeTeachingMethodEnum} + */ + private Integer teachingMethods; + /** + * 是否第一学历 + * + */ + private Boolean firstDegree; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeTrainingExperienceDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeTrainingExperienceDO.java new file mode 100644 index 0000000..afb4be1 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeTrainingExperienceDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工培训经历 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_training_experience") +@KeySequence("hrm_employee_training_experience_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeTrainingExperienceDO extends BaseDO { + + /** + * 培训经历编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 培训课程 + */ + private String course; + /** + * 培训机构名称 + */ + private String organizationName; + /** + * 培训开始日期 + */ + private LocalDateTime startTime; + /** + * 培训结束日期 + */ + private LocalDateTime endTime; + /** + * 培训时长 + */ + private String duration; + /** + * 培训成绩 + */ + private String result; + /** + * 培训证书名称 + */ + private String certificateName; + /** + * 备注 + */ + private String remark; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeWorkExperienceDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeWorkExperienceDO.java new file mode 100644 index 0000000..736ab55 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/experience/HrmEmployeeWorkExperienceDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.experience; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工工作经历 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_work_experience") +@KeySequence("hrm_employee_work_experience_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeWorkExperienceDO extends BaseDO { + + /** + * 工作经历编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 工作单位 + */ + private String workUnit; + /** + * 职务 + */ + private String postName; + /** + * 工作开始日期 + */ + private LocalDateTime startTime; + /** + * 工作结束日期 + */ + private LocalDateTime endTime; + /** + * 离职原因 + */ + private String reason; + /** + * 证明人 + */ + private String witnessName; + /** + * 证明人手机号 + */ + private String witnessPhone; + /** + * 工作备注 + */ + private String remark; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeContactDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeContactDO.java new file mode 100644 index 0000000..8f4a00e --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeContactDO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * HRM 员工联系人 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_contact") +@KeySequence("hrm_employee_contact_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeContactDO extends BaseDO { + + /** + * 联系人编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 联系人名称 + */ + private String name; + /** + * 与员工的关系 + */ + private String relation; + /** + * 联系人电话 + */ + private String phone; + /** + * 联系人工作单位 + */ + private String workUnit; + /** + * 联系人职务 + */ + private String postName; + /** + * 联系人地址 + */ + private String address; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeDO.java new file mode 100644 index 0000000..ef63346 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeDO.java @@ -0,0 +1,213 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.candidate.HrmRecruitCandidateDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.config.HrmRecruitChannelDO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.enums.employee.experience.HrmEmployeeEducationEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeEntryStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeIdTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.common.SexEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工档案 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee") +@KeySequence("hrm_employee_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeDO extends BaseDO { + + /** + * 员工编号 + */ + @TableId + private Long id; + /** + * 员工姓名 + */ + private String name; + /** + * 工号 + */ + private String jobNumber; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserDO#getId()} + * 允许为空,用于待入职、离职历史等没有登录账号的员工档案 + */ + private Long userId; + /** + * 手机号码 + */ + private String mobile; + /** + * 国家或地区 + */ + private String country; + /** + * 民族 + */ + private String nation; + /** + * 证件类型 + * + * 枚举 {@link HrmEmployeeIdTypeEnum} + */ + private Integer idType; + /** + * 证件号码 + */ + private String idNumber; + /** + * 性别 + * + * 枚举 {@link SexEnum} + */ + private Integer sex; + /** + * 邮箱 + */ + private String email; + /** + * 籍贯 + */ + private String nativePlace; + /** + * 出生时间 + */ + private LocalDateTime birthday; + /** + * 年龄 + */ + private Integer age; + /** + * 户籍地址 + */ + private String address; + /** + * 最高学历 + * + * 枚举 {@link HrmEmployeeEducationEnum} + * 字典 {@link DictTypeConstants#HRM_EMPLOYEE_EDUCATION} + */ + private Integer highestEducation; + /** + * 部门编号 + * + * 关联 {@link DeptDO#getId()} + */ + private Long deptId; + /** + * 直属上级员工编号 + * + * 关联 {@link #id} + */ + private Long leaderEmployeeId; + /** + * 入职状态 + * + * 枚举 {@link HrmEmployeeEntryStatusEnum} + * 字典 {@link DictTypeConstants#HRM_EMPLOYEE_ENTRY_STATUS} + */ + private Integer entryStatus; + /** + * 员工状态 + * + * 枚举 {@link HrmEmployeeStatusEnum} + * 字典 {@link DictTypeConstants#HRM_EMPLOYEE_STATUS} + */ + private Integer status; + /** + * 聘用形式 + * + * 枚举 {@link HrmEmployeeTypeEnum} + * 字典 {@link DictTypeConstants#HRM_EMPLOYEE_TYPE} + */ + private Integer type; + /** + * 入职时间 + */ + private LocalDateTime entryTime; + /** + * 试用期,单位:月 + * + * 0 表示无试用期 + */ + private Integer probation; + /** + * 转正时间 + */ + private LocalDateTime regularTime; + /** + * 离职时间 + */ + private LocalDateTime leaveTime; + /** + * 职位名称 + */ + private String postName; + /** + * 岗位职级 + */ + private String postLevel; + /** + * 工作城市 + */ + private String workCity; + /** + * 工作地址 + */ + private String workAddress; + /** + * 工作详细地址 + */ + private String workDetailAddress; + /** + * 招聘渠道编号 + * + * 关联 {@link HrmRecruitChannelDO#getId()} + */ + private Long channelId; + /** + * 司龄开始时间 + */ + private LocalDateTime companyAgeStartTime; + /** + * 司龄,单位:年 + */ + private Integer companyAge; + /** + * 招聘候选人编号 + * + * 关联 {@link HrmRecruitCandidateDO#getId()} + */ + private Long candidateId; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeFileDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeFileDO.java new file mode 100644 index 0000000..637726f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeeFileDO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeFileTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * HRM 员工材料附件 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_file") +@KeySequence("hrm_employee_file_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeeFileDO extends BaseDO { + + /** + * 附件编号 + */ + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 附件类型 + * + * 枚举 {@link HrmEmployeeFileTypeEnum} + */ + private Integer type; + /** + * 附件地址 + */ + private String url; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeePersonalNoteDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeePersonalNoteDO.java new file mode 100644 index 0000000..32b4696 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/info/HrmEmployeePersonalNoteDO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工个人备忘 DO + * + * @author 芋道源码 + */ +@TableName("hrm_employee_personal_note") +@KeySequence("hrm_employee_personal_note_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmEmployeePersonalNoteDO extends BaseDO { + + @TableId + private Long id; + + /** + * 所属员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + + /** + * 备忘内容 + */ + private String content; + + /** + * 提醒时间 + */ + private LocalDateTime reminderTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/config/HrmInsuranceSchemeDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/config/HrmInsuranceSchemeDO.java new file mode 100644 index 0000000..b3b3ecd --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/config/HrmInsuranceSchemeDO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.insurance.config.HrmInsuranceSchemeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * HRM 社保方案 DO + * + * @author 芋道源码 + */ +@TableName("hrm_insurance_scheme") +@KeySequence("hrm_insurance_scheme_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmInsuranceSchemeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 方案名称 + */ + private String name; + /** + * 参保地区编号 + */ + private Integer areaId; + /** + * 户籍类型 + */ + private String householdType; + /** + * 方案类型 + * + * 枚举 {@link HrmInsuranceSchemeTypeEnum} + */ + private Integer type; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/config/HrmInsuranceSchemeProjectDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/config/HrmInsuranceSchemeProjectDO.java new file mode 100644 index 0000000..3250653 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/config/HrmInsuranceSchemeProjectDO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.insurance.config.HrmInsuranceProjectTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * HRM 社保方案项目 DO + * + * @author 芋道源码 + */ +@TableName("hrm_insurance_scheme_project") +@KeySequence("hrm_insurance_scheme_project_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmInsuranceSchemeProjectDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 社保方案编号 + * + * 关联 {@link HrmInsuranceSchemeDO#getId()} + */ + private Long schemeId; + /** + * 项目类型 + * + * 枚举 {@link HrmInsuranceProjectTypeEnum} + */ + private Integer type; + /** + * 项目名称 + */ + private String name; + /** + * 缴纳基数 + */ + private BigDecimal baseAmount; + /** + * 公司缴纳比例 + */ + private BigDecimal corporateRate; + /** + * 个人缴纳比例 + */ + private BigDecimal personalRate; + /** + * 公司缴纳金额 + */ + private BigDecimal corporateAmount; + /** + * 个人缴纳金额 + */ + private BigDecimal personalAmount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/employee/HrmInsuranceEmployeeInfoDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/employee/HrmInsuranceEmployeeInfoDO.java new file mode 100644 index 0000000..d1af7fa --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/employee/HrmInsuranceEmployeeInfoDO.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.employee; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HRM 员工参保信息 DO + * + * @author 芋道源码 + */ +@TableName("hrm_insurance_employee_info") +@KeySequence("hrm_insurance_employee_info_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmInsuranceEmployeeInfoDO extends BaseDO { + + @TableId + private Long id; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 是否首次参保 + */ + private Boolean firstSocialSecurity; + /** + * 是否首次缴纳公积金 + */ + private Boolean firstAccumulationFund; + /** + * 社保账号 + */ + private String socialSecurityNumber; + /** + * 公积金账号 + */ + private String accumulationFundNumber; + /** + * 社保起缴月份 + */ + private LocalDateTime socialSecurityStartMonth; + /** + * 社保方案编号 + * + * 关联 {@link HrmInsuranceSchemeDO#getId()} + */ + private Long schemeId; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/monthrecord/HrmInsuranceMonthEmployeeRecordDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/monthrecord/HrmInsuranceMonthEmployeeRecordDO.java new file mode 100644 index 0000000..9cd25f7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/monthrecord/HrmInsuranceMonthEmployeeRecordDO.java @@ -0,0 +1,146 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.monthrecord; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.config.HrmInsuranceSchemeProjectDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.insurance.employee.HrmInsuranceEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.insurance.config.HrmInsuranceProjectTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * HRM 员工月度社保记录 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_insurance_month_employee_record", autoResultMap = true) +@KeySequence("hrm_insurance_month_employee_record_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmInsuranceMonthEmployeeRecordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 月度社保表编号 + * + * 关联 {@link HrmInsuranceMonthRecordDO#getId()} + */ + private Long monthRecordId; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 社保方案编号 + * + * 关联 {@link HrmInsuranceSchemeDO#getId()} + */ + private Long schemeId; + /** + * 年份 + */ + private Integer year; + /** + * 月份 + */ + private Integer month; + /** + * 个人社保金额 + */ + private BigDecimal personalInsuranceAmount; + /** + * 个人公积金金额 + */ + private BigDecimal personalProvidentFundAmount; + /** + * 公司社保金额 + */ + private BigDecimal corporateInsuranceAmount; + /** + * 公司公积金金额 + */ + private BigDecimal corporateProvidentFundAmount; + /** + * 参保状态 + * + * 枚举 {@link HrmInsuranceEmployeeStatusEnum} + */ + private Integer status; + /** + * 参保项目快照 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List projects; + + /** + * 参保项目快照 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Project { + + /** + * 来源社保方案项目编号 + * + * 关联 {@link HrmInsuranceSchemeProjectDO#getId()} + */ + private Long schemeProjectId; + /** + * 项目类型 + * + * 枚举 {@link HrmInsuranceProjectTypeEnum} + */ + private Integer type; + /** + * 项目名称 + */ + private String name; + /** + * 缴纳基数 + */ + private BigDecimal baseAmount; + /** + * 公司缴纳比例 + */ + private BigDecimal corporateRate; + /** + * 个人缴纳比例 + */ + private BigDecimal personalRate; + /** + * 公司缴纳金额 + */ + private BigDecimal corporateAmount; + /** + * 个人缴纳金额 + */ + private BigDecimal personalAmount; + + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/monthrecord/HrmInsuranceMonthRecordDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/monthrecord/HrmInsuranceMonthRecordDO.java new file mode 100644 index 0000000..3b57cc7 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/insurance/monthrecord/HrmInsuranceMonthRecordDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.insurance.monthrecord; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.insurance.monthrecord.HrmInsuranceMonthStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * HRM 月度社保表 DO + * + * @author 芋道源码 + */ +@TableName("hrm_insurance_month_record") +@KeySequence("hrm_insurance_month_record_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmInsuranceMonthRecordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 标题 + */ + private String title; + /** + * 年份 + */ + private Integer year; + /** + * 月份 + */ + private Integer month; + /** + * 参保人数 + */ + private Integer insuredEmployeeCount; + /** + * 停保人数 + */ + private Integer stoppedEmployeeCount; + /** + * 状态 + * + * 枚举 {@link HrmInsuranceMonthStatusEnum} + */ + private Integer status; + /** + * 个人社保金额 + */ + private BigDecimal personalInsuranceAmount; + /** + * 个人公积金金额 + */ + private BigDecimal personalProvidentFundAmount; + /** + * 公司社保金额 + */ + private BigDecimal corporateInsuranceAmount; + /** + * 公司公积金金额 + */ + private BigDecimal corporateProvidentFundAmount; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentActionRecordDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentActionRecordDO.java new file mode 100644 index 0000000..5652b1f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentActionRecordDO.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAssessmentActionTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAssessmentStageStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * HRM 员工绩效考核动作记录 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_performance_assessment_action_record", autoResultMap = true) +@KeySequence("hrm_performance_assessment_action_record_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmPerformanceAssessmentActionRecordDO extends BaseDO { + + /** + * 动作记录编号 + */ + @TableId + private Long id; + /** + * 员工绩效考核编号 + * + * 关联 {@link HrmPerformanceAssessmentDO#getId()} + */ + private Long assessmentId; + /** + * 员工绩效考核阶段编号 + * + * 关联 {@link HrmPerformanceAssessmentStageDO#getId()} + */ + private Long stageId; + /** + * 操作员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 动作类型 + * + * 枚举 {@link HrmPerformanceAssessmentActionTypeEnum} + */ + private Integer type; + /** + * 动作标题 + */ + private String title; + /** + * 动作内容 + */ + private String content; + /** + * 附件地址列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List fileUrls; + /** + * 动作完成后的阶段状态 + * + * 枚举 {@link HrmPerformanceAssessmentStageStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentAppealRecordDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentAppealRecordDO.java new file mode 100644 index 0000000..8e8b466 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentAppealRecordDO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAppealRecordStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * HRM 员工绩效考核申诉记录 DO + * + * @author 芋道源码 + */ +@TableName("hrm_performance_assessment_appeal_record") +@KeySequence("hrm_performance_assessment_appeal_record_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmPerformanceAssessmentAppealRecordDO extends BaseDO { + + /** + * 申诉记录编号 + */ + @TableId + private Long id; + /** + * 员工绩效考核编号 + * + * 关联 {@link HrmPerformanceAssessmentDO#getId()} + */ + private Long assessmentId; + /** + * 员工绩效考核阶段编号 + * + * 关联 {@link HrmPerformanceAssessmentStageDO#getId()} + */ + private Long stageId; + /** + * 处理状态 + * + * 枚举 {@link HrmPerformanceAppealRecordStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentDO.java new file mode 100644 index 0000000..1d856b2 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentDO.java @@ -0,0 +1,171 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.plan.HrmPerformancePlanDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAppealStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceConfirmationResultEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAssessmentProcessStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceResultAuditStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceStageTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * HRM 员工绩效考核 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_performance_assessment", autoResultMap = true) +@KeySequence("hrm_performance_assessment_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmPerformanceAssessmentDO extends BaseDO { + + /** + * 员工绩效考核编号 + */ + @TableId + private Long id; + /** + * 绩效计划编号 + * + * 关联 {@link HrmPerformancePlanDO#getId()} + */ + private Long planId; + /** + * 员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + /** + * 考核状态 + * + * 枚举 {@link HrmPerformancePlanStatusEnum} + */ + private Integer status; + /** + * 流程处理状态 + * + * 枚举 {@link HrmPerformanceAssessmentProcessStatusEnum} + */ + private Integer processStatus; + /** + * 当前业务阶段类型 + * + * 枚举 {@link HrmPerformanceStageTypeEnum} + */ + private Integer stageType; + /** + * 当前阶段排序 + */ + private Integer stageSort; + /** + * 综合得分 + */ + private BigDecimal score; + /** + * 结果等级 + */ + private String resultLevel; + /** + * 绩效系数 + */ + private BigDecimal coefficient; + /** + * 目标确认结果 + * + * 枚举 {@link HrmPerformanceConfirmationResultEnum} + */ + private Integer targetConfirmationResult; + /** + * 目标确认意见 + */ + private String targetConfirmationComment; + /** + * 目标确认时间 + */ + private LocalDateTime targetConfirmationTime; + /** + * 自评说明 + */ + private String selfComment; + /** + * 他评说明 + */ + private String reviewerComment; + /** + * 结果说明 + */ + private String resultComment; + /** + * 结果确认时间 + */ + private LocalDateTime resultConfirmationTime; + /** + * 结果审核状态 + * + * 枚举 {@link HrmPerformanceResultAuditStatusEnum} + */ + private Integer resultAuditStatus; + /** + * 结果审核时间 + */ + private LocalDateTime resultAuditTime; + /** + * 结果审核意见 + */ + private String resultAuditReason; + + /** + * 申诉原因 + */ + private String appealReason; + /** + * 申诉附件地址列表 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List appealFileUrls; + /** + * 申诉提交时间 + */ + private LocalDateTime appealSubmitTime; + /** + * 申诉状态 + * + * 枚举 {@link HrmPerformanceAppealStatusEnum} + */ + private Integer appealStatus; + /** + * 申诉完成时间 + */ + private LocalDateTime appealTime; + /** + * 申诉处理意见 + */ + private String appealComment; + /** + * 归档时间 + */ + private LocalDateTime archiveTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentDimensionDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentDimensionDO.java new file mode 100644 index 0000000..1ce4457 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentDimensionDO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceQuotaTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * HRM 员工绩效考核维度 DO + * + * @author 芋道源码 + */ +@TableName("hrm_performance_assessment_dimension") +@KeySequence("hrm_performance_assessment_dimension_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmPerformanceAssessmentDimensionDO extends BaseDO { + + /** + * 考核维度编号 + */ + @TableId + private Long id; + /** + * 员工绩效考核编号 + * + * 关联 {@link HrmPerformanceAssessmentDO#getId()} + */ + private Long assessmentId; + /** + * 维度名称 + */ + private String name; + /** + * 指标类型 + * + * 枚举 {@link HrmPerformanceQuotaTypeEnum} + */ + private Integer quotaType; + /** + * 维度权重 + */ + private BigDecimal weight; + /** + * 备注 + */ + private String remark; + /** + * 是否允许员工编辑指标 + */ + private Boolean allowEdit; + /** + * 排序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentQuotaDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentQuotaDO.java new file mode 100644 index 0000000..d958e6f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentQuotaDO.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceQuotaScoreTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * HRM 员工绩效考核指标 DO + * + * @author 芋道源码 + */ +@TableName("hrm_performance_assessment_quota") +@KeySequence("hrm_performance_assessment_quota_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmPerformanceAssessmentQuotaDO extends BaseDO { + + /** + * 考核指标编号 + */ + @TableId + private Long id; + /** + * 员工绩效考核编号 + * + * 关联 {@link HrmPerformanceAssessmentDO#getId()} + */ + private Long assessmentId; + /** + * 考核维度编号 + * + * 关联 {@link HrmPerformanceAssessmentDimensionDO#getId()} + */ + private Long dimensionId; + /** + * 是否预置指标 + */ + private Boolean preset; + /** + * 指标名称 + */ + private String name; + /** + * 指标说明 + */ + private String description; + /** + * 评分标准 + */ + private String standard; + /** + * 指标权重 + */ + private BigDecimal weight; + /** + * 评分类型 + * + * 枚举 {@link HrmPerformanceQuotaScoreTypeEnum} + */ + private Integer scoreType; + /** + * 目标值 + */ + private String targetValue; + /** + * 实际值 + */ + private String actualValue; + /** + * 自评汇总分 + */ + private BigDecimal selfScore; + /** + * 他评汇总分 + */ + private BigDecimal reviewerScore; + /** + * 最终指标得分 + */ + private BigDecimal finalScore; + /** + * 排序 + */ + private Integer sort; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentQuotaScoreDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentQuotaScoreDO.java new file mode 100644 index 0000000..de6c847 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentQuotaScoreDO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.math.BigDecimal; + +/** + * HRM 员工绩效考核指标评分 DO + * + * @author 芋道源码 + */ +@TableName("hrm_performance_assessment_quota_score") +@KeySequence("hrm_performance_assessment_quota_score_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmPerformanceAssessmentQuotaScoreDO extends BaseDO { + + /** + * 考核指标评分编号 + */ + @TableId + private Long id; + /** + * 员工绩效考核阶段编号 + * + * 关联 {@link HrmPerformanceAssessmentStageDO#getId()} + */ + private Long assessmentStageId; + /** + * 员工绩效考核指标编号 + * + * 关联 {@link HrmPerformanceAssessmentQuotaDO#getId()} + */ + private Long assessmentQuotaId; + /** + * 指标得分 + */ + private BigDecimal score; + /** + * 指标评语 + */ + private String comment; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentStageDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentStageDO.java new file mode 100644 index 0000000..54cf5f4 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/assessment/HrmPerformanceAssessmentStageDO.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.assessment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAssessmentStageStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceRaterTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceReviewScoringTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceReviewVisibleContentEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceStageTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * HRM 员工绩效考核阶段 DO + * + * @author 芋道源码 + */ +@TableName("hrm_performance_assessment_stage") +@KeySequence("hrm_performance_assessment_stage_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class HrmPerformanceAssessmentStageDO extends BaseDO { + + /** + * 考核阶段编号 + */ + @TableId + private Long id; + /** + * 员工绩效考核编号 + * + * 关联 {@link HrmPerformanceAssessmentDO#getId()} + */ + private Long assessmentId; + /** + * 业务阶段类型 + * + * 枚举 {@link HrmPerformanceStageTypeEnum} + */ + private Integer type; + /** + * 阶段名称 + */ + private String name; + /** + * 实际处理员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long handlerEmployeeId; + /** + * 评分人类型 + * + * 枚举 {@link HrmPerformanceRaterTypeEnum} + */ + private Integer raterType; + /** + * 评分权重快照 + */ + private BigDecimal weight; + /** + * 评分方式快照 + * + * 枚举 {@link HrmPerformanceReviewScoringTypeEnum} + */ + private Integer scoringType; + /** + * 评分内容可见范围快照 + * + * 枚举 {@link HrmPerformanceReviewVisibleContentEnum} + */ + private Integer visibleContent; + /** + * 评语是否必填 + */ + private Boolean requiredSetting; + /** + * 是否允许驳回 + */ + private Boolean rejectAuthority; + /** + * 排序 + */ + private Integer sort; + /** + * 阶段处理状态 + * + * 枚举 {@link HrmPerformanceAssessmentStageStatusEnum} + */ + private Integer status; + /** + * 阶段汇总得分 + */ + private BigDecimal score; + /** + * 阶段结果等级 + */ + private String resultLevel; + /** + * 阶段意见 + */ + private String comment; + /** + * 最近一次驳回原因 + */ + private String rejectReason; + /** + * 提交时间 + */ + private LocalDateTime submitTime; + /** + * 截止时间 + */ + private LocalDateTime deadlineTime; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/config/HrmPerformanceAssessmentTemplateDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/config/HrmPerformanceAssessmentTemplateDO.java new file mode 100644 index 0000000..94a735c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/config/HrmPerformanceAssessmentTemplateDO.java @@ -0,0 +1,194 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.config; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceQuotaScoreTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceQuotaTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceScoreCalculationEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.config.HrmPerformanceUpperLimitTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * HRM 绩效考核模板 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_performance_assessment_template", autoResultMap = true) +@KeySequence("hrm_performance_assessment_template_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmPerformanceAssessmentTemplateDO extends BaseDO { + + /** + * 绩效考核模板编号 + */ + @TableId + private Long id; + + /** + * 模板名称 + */ + private String name; + /** + * 模板说明 + */ + private String illustrate; + /** + * 计分方式 + * + * 枚举 {@link HrmPerformanceScoreCalculationEnum} + */ + private Integer scoreCalculation; + /** + * 分数上限类型 + * + * 枚举 {@link HrmPerformanceUpperLimitTypeEnum} + */ + private Integer upperLimitType; + /** + * 分数上限 + */ + private BigDecimal upperLimitScore; + /** + * 维度数量 + */ + private Integer dimensionCount; + /** + * 指标数量 + */ + private Integer quotaCount; + /** + * 考核维度配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List dimensions; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 绩效考核配置快照 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class AssessmentConfig { + + /** + * 模板名称 + */ + private String name; + /** + * 计分方式 + * + * 枚举 {@link HrmPerformanceScoreCalculationEnum} + */ + private Integer scoreCalculation; + /** + * 分数上限类型 + * + * 枚举 {@link HrmPerformanceUpperLimitTypeEnum} + */ + private Integer upperLimitType; + /** + * 分数上限 + */ + private BigDecimal upperLimitScore; + /** + * 考核维度配置 + */ + private List dimensions; + } + + /** + * 绩效考核维度 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Dimension { + + /** + * 维度名称 + */ + private String name; + /** + * 指标类型 + * + * 枚举 {@link HrmPerformanceQuotaTypeEnum} + */ + private Integer quotaType; + /** + * 维度权重 + */ + private BigDecimal weight; + /** + * 备注 + */ + private String remark; + /** + * 是否允许员工编辑 + */ + private Boolean allowEdit; + /** + * 考核指标配置 + */ + private List quotas; + } + + /** + * 绩效考核指标 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Quota { + + /** + * 指标名称 + */ + private String name; + /** + * 指标说明 + */ + private String illustrate; + /** + * 评分标准 + */ + private String standard; + /** + * 指标权重 + */ + private BigDecimal weight; + /** + * 评分类型 + * + * 枚举 {@link HrmPerformanceQuotaScoreTypeEnum} + */ + private Integer scoreType; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/config/HrmPerformanceResultTemplateDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/config/HrmPerformanceResultTemplateDO.java new file mode 100644 index 0000000..f120c92 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/config/HrmPerformanceResultTemplateDO.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.config; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.util.List; + +/** + * HRM 绩效结果模板 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_performance_result_template", autoResultMap = true) +@KeySequence("hrm_performance_result_template_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmPerformanceResultTemplateDO extends BaseDO { + + /** + * 绩效结果模板编号 + */ + @TableId + private Long id; + /** + * 模板名称 + */ + private String name; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 结果等级配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List levels; + + /** + * 绩效结果等级 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Level { + + /** + * 等级名称 + */ + private String name; + /** + * 最低分数 + */ + private BigDecimal minScore; + /** + * 最高分数 + */ + private BigDecimal maxScore; + /** + * 绩效系数 + */ + private BigDecimal coefficient; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/plan/HrmPerformancePlanDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/plan/HrmPerformancePlanDO.java new file mode 100644 index 0000000..971f930 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performance/plan/HrmPerformancePlanDO.java @@ -0,0 +1,338 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.performance.plan; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.config.HrmPerformanceAssessmentTemplateDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.performance.config.HrmPerformanceResultTemplateDO.Level; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.employee.info.HrmEmployeeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.assessment.HrmPerformanceAppealTimeoutActionEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceCycleTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanOperationTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanScopeTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformancePlanStatusEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceQuarterEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceQuotaSettingTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceRaterTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceReviewScoringTypeEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceReviewVisibleContentEnum; +import cn.iocoder.yudao.module.hrm.enums.performance.plan.HrmPerformanceStageTypeEnum; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * HRM 绩效考核计划 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_performance_plan", autoResultMap = true) +@KeySequence("hrm_performance_plan_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmPerformancePlanDO extends BaseDO { + + /** + * 计划编号 + */ + @TableId + private Long id; + /** + * 计划状态 + * + * 枚举 {@link HrmPerformancePlanStatusEnum} + */ + private Integer status; + /** + * 当前业务阶段类型 + * + * 枚举 {@link HrmPerformanceStageTypeEnum} + */ + private Integer stageType; + /** + * 当前可执行操作类型 + * + * 枚举 {@link HrmPerformancePlanOperationTypeEnum} + */ + private Integer operationType; + /** + * 终止时间 + */ + private LocalDateTime terminateTime; + + // ==================== 1. 基础设置 ==================== + + /** + * 计划名称 + */ + private String name; + /** + * 考核周期类型 + * + * 枚举 {@link HrmPerformanceCycleTypeEnum} + */ + private Integer cycleType; + /** + * 考核周期 + */ + private String cycle; + /** + * 季度 + * + * 枚举 {@link HrmPerformanceQuarterEnum} + */ + private Integer quarter; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 考核说明 + */ + private String description; + /** + * 考评范围列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + + // ==================== 2. 指标设置 ==================== + + /** + * 考核模板编号 + * + * 关联 {@link HrmPerformanceAssessmentTemplateDO#getId()} + */ + private Long assessmentTemplateId; + /** + * 考核配置快照 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private HrmPerformanceAssessmentTemplateDO.AssessmentConfig assessmentConfig; + + // ==================== 3. 流程设置 ==================== + + /** + * 指标制定类型 + * + * 枚举 {@link HrmPerformanceQuotaSettingTypeEnum} + */ + private Integer quotaSettingType; + /** + * 是否开启目标确认 + */ + private Boolean targetConfirmation; + /** + * 目标确认节点 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private HandlerStage targetConfirmationStage; + /** + * 评分阶段列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List reviewStages; + /** + * 是否开启结果审核 + */ + private Boolean resultAudit; + /** + * 结果审核节点列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List resultAuditStages; + /** + * 是否开启结果确认 + */ + private Boolean resultConfirmation; + /** + * 申诉处理节点列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List appealStages; + /** + * 申诉超期天数 + */ + private Integer appealTimeoutDays; + /** + * 申诉超期处理动作 + * + * 枚举 {@link HrmPerformanceAppealTimeoutActionEnum} + */ + private Integer appealTimeoutAction; + + // ==================== 4. 结果设置 ==================== + + /** + * 结果模板编号 + * + * 关联 {@link HrmPerformanceResultTemplateDO#getId()} + */ + private Long resultTemplateId; + /** + * 结果配置快照 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private ResultConfig resultConfig; + /** + * 是否同步到薪资 + */ + private Boolean syncToSalary; + /** + * 计薪月份 + */ + private String paidForMonth; + + /** + * 绩效计划考评范围 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Scope { + + /** + * 范围类型 + * + * 枚举 {@link HrmPerformancePlanScopeTypeEnum} + */ + private Integer type; + + /** + * 员工编号列表 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private List employeeIds; + /** + * 部门编号列表 + * + * 关联 {@link DeptDO#getId()} + */ + private List deptIds; + /** + * 聘用形式 + * + * 枚举 {@link HrmEmployeeTypeEnum} + */ + private Integer employeeType; + /** + * 员工状态列表 + * + * 枚举 {@link HrmEmployeeStatusEnum} + */ + private List employeeStatuses; + } + + /** + * 绩效计划评分阶段 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ReviewStage { + + /** + * 评分阶段名称 + */ + private String name; + /** + * 评分人 + */ + private HandlerStage rater; + /** + * 评分权重 + */ + private BigDecimal weight; + /** + * 评分方式 + * + * 枚举 {@link HrmPerformanceReviewScoringTypeEnum} + */ + private Integer scoringType; + /** + * 评分内容可见范围 + * + * 枚举 {@link HrmPerformanceReviewVisibleContentEnum} + */ + private Integer visibleContent; + /** + * 评语是否必填 + */ + private Boolean requiredSetting; + /** + * 是否允许驳回 + */ + private Boolean rejectAuthority; + } + + /** + * 绩效计划处理节点 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class HandlerStage { + + /** + * 处理人类型 + * + * 枚举 {@link HrmPerformanceRaterTypeEnum} + */ + private Integer type; + /** + * 上级或部门层级 + */ + private Integer level; + /** + * 指定处理员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long employeeId; + } + + /** + * 绩效结果配置快照 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ResultConfig { + + /** + * 模板名称 + */ + private String name; + /** + * 结果等级配置 + */ + private List levels; + } + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/candidate/HrmRecruitCandidateDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/candidate/HrmRecruitCandidateDO.java new file mode 100644 index 0000000..151b61f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/candidate/HrmRecruitCandidateDO.java @@ -0,0 +1,129 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.candidate; + +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.config.HrmRecruitChannelDO; +import cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post.HrmRecruitPostDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.enums.recruit.candidate.HrmRecruitCandidateStatusEnum; +import cn.iocoder.yudao.module.system.enums.common.SexEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * HRM 招聘候选人 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_recruit_candidate", autoResultMap = true) +@KeySequence("hrm_recruit_candidate_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmRecruitCandidateDO extends BaseDO { + + /** + * 候选人编号 + */ + @TableId + private Long id; + /** + * 候选人姓名 + */ + private String name; + /** + * 手机号码 + */ + private String mobile; + /** + * 性别 + * + * 枚举 {@link SexEnum} + */ + private Integer sex; + /** + * 年龄 + */ + private Integer age; + /** + * 邮箱 + */ + private String email; + /** + * 应聘职位编号 + * + * 关联 {@link HrmRecruitPostDO#getId()} + */ + private Long postId; + /** + * 面试轮次 + */ + private Integer stageNumber; + /** + * 工作年限,单位:年 + */ + private Integer workTime; + /** + * 学历 + * + * 字典 {@link DictTypeConstants#HRM_RECRUIT_CANDIDATE_EDUCATION} + */ + private Integer education; + /** + * 毕业院校 + */ + private String graduateSchool; + /** + * 最近工作单位 + */ + private String latestWorkPlace; + /** + * 招聘渠道编号 + * + * 关联 {@link HrmRecruitChannelDO#getId()} + */ + private Long channelId; + /** + * 备注 + */ + private String remark; + /** + * 候选人状态 + * + * 枚举 {@link HrmRecruitCandidateStatusEnum} + * 字典 {@link DictTypeConstants#HRM_RECRUIT_CANDIDATE_STATUS} + */ + private Integer status; + /** + * 淘汰原因 + */ + private String eliminate; + /** + * 状态更新时间 + */ + private LocalDateTime statusUpdateTime; + /** + * 入职时间 + */ + private LocalDateTime entryTime; + /** + * 简历附件地址数组 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List resumeUrls; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/candidate/HrmRecruitInterviewDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/candidate/HrmRecruitInterviewDO.java new file mode 100644 index 0000000..40beac0 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/candidate/HrmRecruitInterviewDO.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.candidate; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.enums.recruit.candidate.HrmRecruitInterviewResultEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * HRM 招聘面试记录 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_recruit_interview", autoResultMap = true) +@KeySequence("hrm_recruit_interview_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmRecruitInterviewDO extends BaseDO { + + /** + * 面试编号 + */ + @TableId + private Long id; + /** + * 候选人编号 + * + * 关联 {@link HrmRecruitCandidateDO#getId()} + */ + private Long candidateId; + /** + * 面试方式 + * + * 字典 {@link DictTypeConstants#HRM_RECRUIT_INTERVIEW_TYPE} + */ + private Integer type; + /** + * 面试轮次 + */ + private Integer stageNumber; + /** + * 主面试官员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long interviewEmployeeId; + /** + * 其他面试官员工编号数组 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List otherInterviewEmployeeIds; + /** + * 面试时间 + */ + private LocalDateTime interviewTime; + /** + * 面试地址 + */ + private String address; + /** + * 备注 + */ + private String remark; + /** + * 面试结果 + * + * 枚举 {@link HrmRecruitInterviewResultEnum} + * 字典 {@link DictTypeConstants#HRM_RECRUIT_INTERVIEW_RESULT} + */ + private Integer result; + /** + * 面试评价 + */ + private String evaluate; + /** + * 取消原因 + */ + private String cancelReason; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/config/HrmRecruitChannelDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/config/HrmRecruitChannelDO.java new file mode 100644 index 0000000..bbcc17c --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/config/HrmRecruitChannelDO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.config; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * HRM 招聘渠道 DO + * + * @author 芋道源码 + */ +@TableName("hrm_recruit_channel") +@KeySequence("hrm_recruit_channel_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmRecruitChannelDO extends BaseDO { + + /** + * 招聘渠道编号 + */ + @TableId + private Long id; + /** + * 是否系统内置 + */ + private Boolean systemFlag; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 渠道名称 + */ + private String name; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/post/HrmRecruitPostDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/post/HrmRecruitPostDO.java new file mode 100644 index 0000000..afee29f --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/post/HrmRecruitPostDO.java @@ -0,0 +1,175 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; +import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants; +import cn.iocoder.yudao.module.hrm.enums.recruit.post.HrmRecruitPostStatusEnum; +import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * HRM 招聘职位 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_recruit_post", autoResultMap = true) +@KeySequence("hrm_recruit_post_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmRecruitPostDO extends BaseDO { + + /** + * 薪资 - 面议值 + */ + public static final BigDecimal SALARY_NEGOTIABLE_VALUE = BigDecimal.valueOf(-1); + /** + * 薪资 - 面议单位值 + */ + public static final Integer SALARY_NEGOTIABLE_UNIT_VALUE = -1; + + /** + * 年龄 - 不限值 + */ + public static final Integer AGE_UNLIMITED_VALUE = -1; + + /** + * 招聘职位编号 + */ + @TableId + private Long id; + /** + * 职位名称 + */ + private String postName; + /** + * 部门编号 + * + * 关联 {@link DeptDO#getId()} + */ + private Long deptId; + /** + * 工作性质 + * + * 字典 {@link DictTypeConstants#HRM_RECRUIT_JOB_NATURE} + */ + private Integer jobNature; + /** + * 工作城市地区编号 + * + * 关联 {@link cn.iocoder.yudao.framework.ip.core.Area#getId()} + */ + private Integer areaId; + /** + * 招聘人数 + */ + private Integer recruitNum; + /** + * 招聘原因 + */ + private String reason; + /** + * 工作经验要求 + * + * 字典 {@link DictTypeConstants#HRM_RECRUIT_WORK_TIME} + */ + private Integer workTime; + /** + * 学历要求 + * + * 字典 {@link DictTypeConstants#HRM_RECRUIT_POST_EDUCATION} + */ + private Integer educationRequire; + /** + * 最低薪资 + * + * -1 表示面议 + */ + private BigDecimal minSalary; + /** + * 最高薪资 + * + * -1 表示面议 + */ + private BigDecimal maxSalary; + /** + * 薪资单位 + * + * 字典 {@link DictTypeConstants#HRM_RECRUIT_SALARY_UNIT} + */ + private Integer salaryUnit; + /** + * 最小年龄 + * + * -1 表示不限 + */ + private Integer minAge; + /** + * 最大年龄 + * + * -1 表示不限 + */ + private Integer maxAge; + /** + * 最迟到岗时间 + */ + private LocalDateTime latestEntryTime; + /** + * 招聘负责人员工编号 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + private Long ownerEmployeeId; + /** + * 面试官员工编号数组 + * + * 关联 {@link HrmEmployeeDO#getId()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List interviewEmployeeIds; + /** + * 职位描述 + */ + private String description; + /** + * 紧急程度 + * + * 字典 {@link DictTypeConstants#HRM_RECRUIT_EMERGENCY_LEVEL} + */ + private Integer emergencyLevel; + /** + * 职位类型编号 + * + * 关联 {@link HrmRecruitPostTypeDO#getId()} + */ + private Long postTypeId; + /** + * 职位状态 + * + * 枚举 {@link HrmRecruitPostStatusEnum} + * 字典 {@link DictTypeConstants#HRM_RECRUIT_POST_STATUS} + */ + private Integer status; + /** + * 停止原因 + */ + private String stopReason; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/post/HrmRecruitPostTypeDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/post/HrmRecruitPostTypeDO.java new file mode 100644 index 0000000..13944fc --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/recruit/post/HrmRecruitPostTypeDO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.recruit.post; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * HRM 招聘职位类型 DO + * + * @author 芋道源码 + */ +@TableName("hrm_recruit_post_type") +@KeySequence("hrm_recruit_post_type_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmRecruitPostTypeDO extends BaseDO { + + /** + * 职位类型编号 + */ + @TableId + private Long id; + /** + * 类型名称 + */ + private String name; + /** + * 父类型编号 + * + * 关联 {@link #id} + */ + private Long parentId; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/config/HrmSalaryChangeTemplateDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/config/HrmSalaryChangeTemplateDO.java new file mode 100644 index 0000000..58647c5 --- /dev/null +++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/config/HrmSalaryChangeTemplateDO.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.hrm.dal.dataobject.salary.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * HRM 调薪模板 DO + * + * @author 芋道源码 + */ +@TableName(value = "hrm_salary_change_template", autoResultMap = true) +@KeySequence("hrm_salary_change_template_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HrmSalaryChangeTemplateDO extends BaseDO { + + /** + * 调薪模板编号 + */ + @TableId + private Long id; + /** + * 模板名称 + */ + private String name; + /** + * 是否默认模板 + */ + private Boolean defaultStatus; + /** + * 调薪项配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List