package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.Money; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.FinVoucherTemplate; import com.kaidi.oa.domain.Voucher; import com.kaidi.oa.repository.FinVoucherTemplateRepository; import com.kaidi.oa.repository.VoucherRepository; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.Instant; import java.time.LocalDate; import java.util.List; import java.util.Map; /** * 财务部·凭证模板管理与期末结转(财务总账深化)。 * * 审计缺口:无凭证模板;无自动/自定义结转损益;无期末处理。 * * 本控制器提供: * 1. 凭证模板 CRUD(常用业务:计提折旧/摊销/结转损益/结转研发/分摊制造费/期末调汇)。 * 2. POST /{id}/apply —— 按模板一键生成草稿凭证(摘要变量替换 ${period}/${amount})。 * 3. POST /period-close —— 月末结转损益:自动生成一批结转凭证(收入→本年利润/成本→本年利润), * 返回生成的凭证列表(草稿状态,需财务审核过账)。 * * 写口:AuthInterceptor FINANCE_PREFIXES(/api/oa/fin-voucher-templates) 限 ADMIN/APPROVER。 * 读口:SENSITIVE_READ_PREFIXES 同门槛(含科目信息,属机密财务)。 */ @RestController @RequestMapping("/api/oa/fin-voucher-templates") public class FinVoucherTemplateController { private final FinVoucherTemplateRepository templateRepo; private final VoucherRepository voucherRepo; public FinVoucherTemplateController(FinVoucherTemplateRepository templateRepo, VoucherRepository voucherRepo) { this.templateRepo = templateRepo; this.voucherRepo = voucherRepo; } // ---------- 模板 CRUD ---------- @GetMapping public ApiResp> list( @RequestParam(required = false) String templateType, @RequestParam(required = false) Boolean enabled) { if (templateType != null && !templateType.isBlank()) { return ApiResp.ok(templateRepo.findByTemplateType(templateType)); } if (enabled != null) { return ApiResp.ok(templateRepo.findByEnabled(enabled)); } return ApiResp.ok(templateRepo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(templateRepo.findById(id) .orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id))); } public record TemplateRequest( String name, String templateType, String debitAccount, String creditAccount, String summaryTemplate, String amountSource, Boolean enabled, String createdBy) { } @PostMapping public ApiResp create(@RequestBody TemplateRequest req) { if (req.name() == null || req.name().isBlank()) { throw new ApiException(400, "模板名称不能为空"); } FinVoucherTemplate t = new FinVoucherTemplate(); t.setCode("TMPl-" + (templateRepo.count() + 1)); t.setName(req.name()); t.setTemplateType(req.templateType() == null ? FinVoucherTemplate.TYPE_MANUAL : req.templateType()); t.setDebitAccount(req.debitAccount()); t.setCreditAccount(req.creditAccount()); t.setSummaryTemplate(req.summaryTemplate() == null ? req.name() : req.summaryTemplate()); t.setAmountSource(req.amountSource() == null ? "手填" : req.amountSource()); t.setEnabled(req.enabled() == null ? Boolean.TRUE : req.enabled()); t.setCreatedBy(req.createdBy()); t.setCreatedAt(Instant.now()); return ApiResp.ok(templateRepo.save(t)); } @PutMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody TemplateRequest req) { FinVoucherTemplate t = templateRepo.findById(id) .orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id)); if (req.name() != null && !req.name().isBlank()) t.setName(req.name()); if (req.templateType() != null) t.setTemplateType(req.templateType()); if (req.debitAccount() != null) t.setDebitAccount(req.debitAccount()); if (req.creditAccount() != null) t.setCreditAccount(req.creditAccount()); if (req.summaryTemplate() != null) t.setSummaryTemplate(req.summaryTemplate()); if (req.amountSource() != null) t.setAmountSource(req.amountSource()); if (req.enabled() != null) t.setEnabled(req.enabled()); return ApiResp.ok(templateRepo.save(t)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { templateRepo.findById(id).orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id)); templateRepo.deleteById(id); return ApiResp.ok(null); } // ---------- 按模板生成草稿凭证 ---------- public record ApplyRequest(Double amount, String period, String preparer) {} @PostMapping("/{id}/apply") @Transactional public ApiResp apply(@PathVariable Long id, @RequestBody ApplyRequest req) { FinVoucherTemplate t = templateRepo.findById(id) .orElseThrow(() -> new NotFoundException("凭证模板不存在: " + id)); if (!Boolean.TRUE.equals(t.getEnabled())) { throw new ApiException(400, "该模板已停用,无法生成凭证"); } if (req.amount() == null || req.amount() <= 0) { throw new ApiException(400, "金额必须大于 0"); } String period = req.period() != null ? req.period() : LocalDate.now().toString().substring(0, 7); String summary = (t.getSummaryTemplate() == null ? t.getName() : t.getSummaryTemplate()) .replace("${period}", period) .replace("${amount}", String.format("%.2f", req.amount())); Voucher v = new Voucher(); v.setVoucherNo("TMPl-" + t.getCode() + "-" + System.currentTimeMillis() % 10000); v.setVoucherDate(LocalDate.now().toString()); v.setSummary(summary); v.setDebitAccount(t.getDebitAccount()); v.setCreditAccount(t.getCreditAccount()); v.setAmount(Money.of(req.amount())); v.setStatus(Voucher.S_DRAFT); v.setPreparer(req.preparer()); v.setReversed(Boolean.FALSE); v.setIsReversal(Boolean.FALSE); v.setCreatedAt(Instant.now()); return ApiResp.ok(voucherRepo.save(v)); } // ---------- 月末结转损益 ---------- public record PeriodCloseRequest( String period, String preparer, Double totalRevenue, Double totalCost, Double totalExpense) {} /** * 月末结转损益:生成三张草稿凭证(借收入/贷本年利润;借本年利润/贷成本;借本年利润/贷费用), * 财务确认后逐张审核过账完成结账前检查。 */ @PostMapping("/period-close") @Transactional public ApiResp> periodClose(@RequestBody PeriodCloseRequest req) { if (req.period() == null || req.period().isBlank()) { throw new ApiException(400, "结转期间不能为空,格式 yyyy-MM"); } String period = req.period(); String preparer = req.preparer() == null ? "系统自动" : req.preparer(); double revenue = req.totalRevenue() == null ? 0 : req.totalRevenue(); double cost = req.totalCost() == null ? 0 : req.totalCost(); double expense = req.totalExpense() == null ? 0 : req.totalExpense(); List generated = new java.util.ArrayList<>(); // 凭证1:结转收入 → 本年利润(借 6001/主营业务收入,贷 4103/本年利润) if (revenue > 0) { Voucher v1 = buildTransferVoucher( "JZ-" + period + "-01", period, period + " 月结转主营业务收入", "6001", "4103", revenue, preparer); generated.add(voucherRepo.save(v1)); } // 凭证2:结转成本 → 本年利润(借 4103/本年利润,贷 6401/主营业务成本) if (cost > 0) { Voucher v2 = buildTransferVoucher( "JZ-" + period + "-02", period, period + " 月结转主营业务成本", "4103", "6401", cost, preparer); generated.add(voucherRepo.save(v2)); } // 凭证3:结转期间费用 → 本年利润(借 4103/本年利润,贷 6601/管理费用+销售费用+财务费用汇总) if (expense > 0) { Voucher v3 = buildTransferVoucher( "JZ-" + period + "-03", period, period + " 月结转期间费用", "4103", "6601", expense, preparer); generated.add(voucherRepo.save(v3)); } if (generated.isEmpty()) { throw new ApiException(400, "收入/成本/费用至少填写一项"); } return ApiResp.ok(generated); } private Voucher buildTransferVoucher(String voucherNo, String period, String summary, String debit, String credit, double amount, String preparer) { Voucher v = new Voucher(); v.setVoucherNo(voucherNo); v.setVoucherDate(LocalDate.now().toString()); v.setSummary(summary); v.setDebitAccount(debit); v.setCreditAccount(credit); v.setAmount(Money.of(amount)); v.setStatus(Voucher.S_DRAFT); v.setPreparer(preparer); v.setReversed(Boolean.FALSE); v.setIsReversal(Boolean.FALSE); v.setCreatedAt(Instant.now()); return v; } // ---------- 模板类型列表(前端下拉用) ---------- @GetMapping("/template-types") public ApiResp> templateTypes() { return ApiResp.ok(List.of( FinVoucherTemplate.TYPE_MANUAL, FinVoucherTemplate.TYPE_DEPREC, FinVoucherTemplate.TYPE_AMORT, FinVoucherTemplate.TYPE_TRANSFER, FinVoucherTemplate.TYPE_RD, FinVoucherTemplate.TYPE_MFG, FinVoucherTemplate.TYPE_FX )); } }