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") +);