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.FinImpairmentTest; import com.kaidi.oa.domain.Voucher; import com.kaidi.oa.repository.FinImpairmentTestRepository; 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.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.util.List; /** * 财务部·资产减值测试(财务管理·资产管理 深化,MED 缺口 #7)。 * * 审计缺口:资产减值测试/计提减值准备无专属实体;减值审批流缺失; * 计提减值准备凭证自动生成缺失。 * * 端点: * GET / —— 减值测试台账列表。 * GET /{id} —— 单条明细。 * POST / —— 新建减值测试(自动计算减值损失)。 * PUT /{id} —— 编辑(草稿态)。 * DELETE /{id} —— 删除(草稿态)。 * POST /{id}/approve —— 审批通过(草稿→已审批)。 * POST /{id}/post —— 计提过账:生成减值计提凭证(已审批→已计提)。 * * 写口:AuthInterceptor FINANCE_PREFIXES(/api/oa/fin-impairment-tests) ADMIN/APPROVER。 */ @RestController @RequestMapping("/api/oa/fin-impairment-tests") public class FinImpairmentTestController { private final FinImpairmentTestRepository testRepo; private final VoucherRepository voucherRepo; public FinImpairmentTestController(FinImpairmentTestRepository testRepo, VoucherRepository voucherRepo) { this.testRepo = testRepo; this.voucherRepo = voucherRepo; } @GetMapping public ApiResp> list( @RequestParam(required = false) String statusVal, @RequestParam(required = false) String assetTypeVal) { if (statusVal != null && !statusVal.isBlank()) return ApiResp.ok(testRepo.findByStatusVal(statusVal)); if (assetTypeVal != null && !assetTypeVal.isBlank()) return ApiResp.ok(testRepo.findByAssetTypeVal(assetTypeVal)); return ApiResp.ok(testRepo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(testRepo.findById(id) .orElseThrow(() -> new NotFoundException("减值测试记录不存在: " + id))); } public record ImpairmentRequest( Long assetId, String assetName, String assetTypeVal, String testDate, Double bookValue, Double recoverableAmount, Double accumulatedImpairment, String testBasis, String preparer) { } @PostMapping public ApiResp create(@RequestBody ImpairmentRequest req) { if (req.assetName() == null || req.assetName().isBlank()) throw new ApiException(400, "资产名称(assetName)不能为空"); if (req.bookValue() == null) throw new ApiException(400, "账面价值(bookValue)不能为空"); FinImpairmentTest t = build(req); t.setStatusVal(FinImpairmentTest.STATUS_DRAFT); t.setCreatedAt(Instant.now()); recalc(t); return ApiResp.ok(testRepo.save(t)); } @PutMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody ImpairmentRequest req) { FinImpairmentTest t = testRepo.findById(id) .orElseThrow(() -> new NotFoundException("减值测试记录不存在: " + id)); if (!FinImpairmentTest.STATUS_DRAFT.equals(t.getStatusVal())) throw new ApiException(409, "仅草稿态减值测试可编辑"); if (req.assetId() != null) t.setAssetId(req.assetId()); if (req.assetName() != null) t.setAssetName(req.assetName()); if (req.assetTypeVal() != null) t.setAssetTypeVal(req.assetTypeVal()); if (req.testDate() != null) t.setTestDate(req.testDate()); if (req.bookValue() != null) t.setBookValue(Money.of(req.bookValue())); if (req.recoverableAmount() != null) t.setRecoverableAmount(Money.of(req.recoverableAmount())); if (req.accumulatedImpairment() != null) t.setAccumulatedImpairment(Money.of(req.accumulatedImpairment())); if (req.testBasis() != null) t.setTestBasis(req.testBasis()); if (req.preparer() != null) t.setPreparer(req.preparer()); recalc(t); return ApiResp.ok(testRepo.save(t)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { FinImpairmentTest t = testRepo.findById(id) .orElseThrow(() -> new NotFoundException("减值测试记录不存在: " + id)); if (!FinImpairmentTest.STATUS_DRAFT.equals(t.getStatusVal())) throw new ApiException(409, "仅草稿态减值测试可删除"); testRepo.deleteById(id); return ApiResp.ok(null); } public record ApproveRequest(String approver) {} /** 审批通过:草稿 → 已审批。 */ @PostMapping("/{id}/approve") @Transactional public ApiResp approve(@PathVariable Long id, @RequestBody(required = false) ApproveRequest req) { FinImpairmentTest t = testRepo.findById(id) .orElseThrow(() -> new NotFoundException("减值测试记录不存在: " + id)); if (!FinImpairmentTest.STATUS_DRAFT.equals(t.getStatusVal())) throw new ApiException(409, "仅草稿态可审批"); if (req != null && req.approver() != null) t.setApprover(req.approver()); t.setStatusVal(FinImpairmentTest.STATUS_APPROVED); return ApiResp.ok(testRepo.save(t)); } /** * 计提过账:已审批 → 已计提。 * 自动生成减值计提凭证(借 资产减值损失 贷 固定资产减值准备)。 */ @PostMapping("/{id}/post") @Transactional public ApiResp post(@PathVariable Long id) { FinImpairmentTest t = testRepo.findById(id) .orElseThrow(() -> new NotFoundException("减值测试记录不存在: " + id)); if (!FinImpairmentTest.STATUS_APPROVED.equals(t.getStatusVal())) throw new ApiException(409, "仅已审批的减值测试可过账计提"); BigDecimal loss = Money.nz(t.getImpairmentLoss()); if (loss.compareTo(BigDecimal.ZERO) <= 0) throw new ApiException(400, "减值损失为 0,无需计提凭证"); // 生成减值计提凭证 Voucher v = new Voucher(); v.setVoucherNo("JZ-" + System.currentTimeMillis() % 100000); v.setVoucherDate(LocalDate.now().toString()); v.setSummary("资产减值计提:" + t.getAssetName()); v.setDebitAccount("8701 资产减值损失"); v.setCreditAccount("1602 固定资产减值准备"); v.setAmount(loss); v.setStatus(Voucher.S_POSTED); v.setPreparer(t.getPreparer()); v.setIsReversal(false); v.setReversed(false); v.setSourceType("impairment"); v.setSourceId(t.getId()); v.setCreatedAt(Instant.now()); Voucher saved = voucherRepo.save(v); t.setVoucherId(saved.getId()); t.setStatusVal(FinImpairmentTest.STATUS_POSTED); // 更新累计减值准备 t.setAccumulatedImpairment(Money.add(t.getAccumulatedImpairment(), loss)); return ApiResp.ok(testRepo.save(t)); } // ---------- helpers ---------- private FinImpairmentTest build(ImpairmentRequest req) { FinImpairmentTest t = new FinImpairmentTest(); t.setAssetId(req.assetId()); t.setAssetName(req.assetName()); t.setAssetTypeVal(req.assetTypeVal() != null ? req.assetTypeVal() : "固定资产"); t.setTestDate(req.testDate() != null ? req.testDate() : LocalDate.now().toString()); t.setBookValue(Money.of(req.bookValue())); t.setRecoverableAmount(Money.of(req.recoverableAmount())); t.setAccumulatedImpairment(Money.of(req.accumulatedImpairment())); t.setTestBasis(req.testBasis()); t.setPreparer(req.preparer()); return t; } /** 减值损失 = max(0, 账面价值 - 可收回金额)。 */ private void recalc(FinImpairmentTest t) { BigDecimal bv = Money.nz(t.getBookValue()); BigDecimal ra = Money.nz(t.getRecoverableAmount()); BigDecimal loss = bv.subtract(ra); t.setImpairmentLoss(loss.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : loss); } }