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.Budget; import com.kaidi.oa.domain.RdGrantDisbursement; import com.kaidi.oa.domain.Voucher; import com.kaidi.oa.repository.BudgetRepository; import com.kaidi.oa.repository.RdGrantDisbursementRepository; import com.kaidi.oa.repository.VoucherRepository; import jakarta.servlet.http.HttpServletRequest; import org.springframework.transaction.annotation.Transactional; 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.util.List; /** * 政府资金到账·财务入账与项目预算提取联动(创新研发中心·申报服务部,需求 §3 资金拨付跟踪)。 * * 补 PARTIAL(med):原到账仅生成机构服务费付款单,未联动「财务凭证入账」「项目支出预算提取」。 * 本控制器以「已到账」的 {@link RdGrantDisbursement} 为源,提供两条到账后联动动作(加法式,不改原控制器): * * 金额一律 {@link Money}(BigDecimal);级联写带 {@link Transactional}。 */ @RestController @RequestMapping("/api/oa/decl-grant-ledger") public class DeclGrantLedgerController { private static final String ST_RECEIVED = "已到账"; private static final String ST_SETTLED = "已分配"; private static final String SRC_GRANT_VOUCHER = "rd_grant_receipt"; private static final String SRC_GRANT_BUDGET = "政府补助资金"; private final RdGrantDisbursementRepository grantRepo; private final VoucherRepository voucherRepo; private final BudgetRepository budgetRepo; private final CurrentUserResolver currentUser; public DeclGrantLedgerController(RdGrantDisbursementRepository grantRepo, VoucherRepository voucherRepo, BudgetRepository budgetRepo, CurrentUserResolver currentUser) { this.grantRepo = grantRepo; this.voucherRepo = voucherRepo; this.budgetRepo = budgetRepo; this.currentUser = currentUser; } /** 某笔资金的入账与预算提取联动状态(前端判断按钮可用性)。 */ public record LedgerStatus(Long grantId, String projectName, double receivedAmount, boolean vouchered, Long voucherId, String voucherNo, boolean budgetExtracted, Long budgetId) { } @GetMapping("/{grantId}/status") public ApiResp status(@PathVariable Long grantId) { RdGrantDisbursement g = loadGrant(grantId); Voucher v = findVoucher(grantId); Budget b = findBudget(grantId); return ApiResp.ok(new LedgerStatus(g.getId(), g.getProjectName(), Money.nz(g.getReceivedAmount()).doubleValue(), v != null, v == null ? null : v.getId(), v == null ? null : v.getVoucherNo(), b != null, b == null ? null : b.getId())); } /** 生成政府资金到账记账凭证:借 银行存款 / 贷 营业外收入-政府补助。要求资金已到账、同源未入账。 */ @PostMapping("/{grantId}/post-voucher") @Transactional public ApiResp postVoucher(@PathVariable Long grantId, HttpServletRequest request) { RdGrantDisbursement g = loadGrant(grantId); requireReceived(g); if (findVoucher(grantId) != null) { throw new ApiException(409, "该笔资金已生成到账入账凭证,请勿重复"); } BigDecimal amount = Money.nz(g.getReceivedAmount()); if (Money.lte0(amount)) { throw new ApiException(400, "到账金额为 0,无法入账"); } Voucher v = new Voucher(); v.setVoucherNo("PZ-ZJ" + g.getId()); v.setVoucherDate(LocalDate.now().toString()); v.setSummary("收到政府资助资金到账:" + g.getProjectName() + (g.getAuthority() == null ? "" : "(" + g.getAuthority() + ")")); v.setDebitAccount("银行存款"); v.setCreditAccount("营业外收入-政府补助"); v.setAmount(amount); v.setStatus("待审核"); v.setPreparer(currentUser.resolveLabel(request)); v.setSourceType(SRC_GRANT_VOUCHER); v.setSourceId(g.getId()); v.setCreatedAt(Instant.now()); return ApiResp.ok(voucherRepo.save(v)); } /** 按到账资金提取项目支出预算(element=政府补助资金)。要求资金已到账、同源未提取。 */ @PostMapping("/{grantId}/extract-budget") @Transactional public ApiResp extractBudget(@PathVariable Long grantId, HttpServletRequest request) { RdGrantDisbursement g = loadGrant(grantId); requireReceived(g); if (findBudget(grantId) != null) { throw new ApiException(409, "该笔资金已提取项目支出预算,请勿重复"); } BigDecimal amount = Money.nz(g.getReceivedAmount()); if (Money.lte0(amount)) { throw new ApiException(400, "到账金额为 0,无法提取预算"); } Budget b = new Budget(); b.setName("政府补助专项预算·" + g.getProjectName()); b.setProjectId(g.getPolicyApplicationId()); b.setCompanySubject(g.getCompanySubject()); b.setElement(SRC_GRANT_BUDGET); b.setYear(String.valueOf(LocalDate.now().getYear())); b.setPeriod("年度"); b.setBudgetAmount(amount); b.setActualAmount(Money.ZERO); b.setOwner(g.getOwner() == null || g.getOwner().isBlank() ? currentUser.resolveLabel(request) : g.getOwner()); b.setStatus("执行中"); b.setCreatedAt(Instant.now()); // 复用 name 末尾标注来源 grant id,便于同源幂等检测(避免改动共享 Budget 实体加新列)。 b.setName(b.getName() + " #G" + g.getId()); return ApiResp.ok(budgetRepo.save(b)); } // ---------- helpers ---------- private RdGrantDisbursement loadGrant(Long id) { return grantRepo.findById(id) .orElseThrow(() -> new NotFoundException("grant disbursement not found: " + id)); } private void requireReceived(RdGrantDisbursement g) { if (!ST_RECEIVED.equals(g.getStatus()) && !ST_SETTLED.equals(g.getStatus())) { throw new ApiException(400, "资金尚未全额到账,不能入账/提取预算(当前:" + g.getStatus() + ")"); } } /** 同源凭证检测:source=rd_grant_receipt 且 sourceId=grantId。 */ private Voucher findVoucher(Long grantId) { for (Voucher v : voucherRepo.findAll()) { if (SRC_GRANT_VOUCHER.equals(v.getSourceType()) && grantId.equals(v.getSourceId())) { return v; } } return null; } /** 同源预算检测:element=政府补助资金 且 name 末尾标注 #G。 */ private Budget findBudget(Long grantId) { String tag = "#G" + grantId; for (Budget b : budgetRepo.findByElement(SRC_GRANT_BUDGET)) { if (b.getName() != null && b.getName().endsWith(tag)) { return b; } } return null; } }