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.FinAdvance; import com.kaidi.oa.domain.Voucher; import com.kaidi.oa.repository.FinAdvanceRepository; 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; import java.util.Map; import java.util.LinkedHashMap; /** * 财务部·预收款 / 预付款管理(应收应付扩展场景)。 * * 审计缺口:预收款场景未实现。 * * 本控制器提供: * 1. 预收/预付台账 CRUD(登记预收款/预付款单)。 * 2. POST /{id}/settle —— 核销:将预收款与对应应收单核销,累加核销额,状态机推进。 * 3. POST /{id}/voucher —— 自动生成收/付款凭证(借 银行存款 / 贷 预收账款,或相反)。 * 4. GET /summary —— 预收/预付汇总(待核销/已核销金额统计)。 * * 写口:AuthInterceptor FINANCE_PREFIXES(/api/oa/fin-advances) 限 ADMIN/APPROVER。 */ @RestController @RequestMapping("/api/oa/fin-advances") public class FinAdvanceController { private final FinAdvanceRepository advRepo; private final VoucherRepository voucherRepo; public FinAdvanceController(FinAdvanceRepository advRepo, VoucherRepository voucherRepo) { this.advRepo = advRepo; this.voucherRepo = voucherRepo; } @GetMapping public ApiResp> list( @RequestParam(required = false) String advanceType, @RequestParam(required = false) String status) { if (advanceType != null && status != null) { return ApiResp.ok(advRepo.findByAdvanceTypeAndStatus(advanceType, status)); } if (advanceType != null) return ApiResp.ok(advRepo.findByAdvanceType(advanceType)); if (status != null) return ApiResp.ok(advRepo.findByStatus(status)); return ApiResp.ok(advRepo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(advRepo.findById(id) .orElseThrow(() -> new NotFoundException("预收/预付记录不存在: " + id))); } public record AdvanceRequest( String advanceType, String partyName, String contractRef, Double amount, String receiptDate, String remark, String operator) { } @PostMapping public ApiResp create(@RequestBody AdvanceRequest req) { if (req.advanceType() == null || (!FinAdvance.T_RECEIVED.equals(req.advanceType()) && !FinAdvance.T_PAID.equals(req.advanceType()))) { throw new ApiException(400, "类型(advanceType)必须为「预收」或「预付」"); } if (req.partyName() == null || req.partyName().isBlank()) { throw new ApiException(400, "往来单位(partyName)不能为空"); } if (req.amount() == null || req.amount() <= 0) { throw new ApiException(400, "金额(amount)必须大于 0"); } FinAdvance a = new FinAdvance(); a.setCode((FinAdvance.T_RECEIVED.equals(req.advanceType()) ? "YS-" : "YF-") + (advRepo.count() + 1)); a.setAdvanceType(req.advanceType()); a.setPartyName(req.partyName()); a.setContractRef(req.contractRef()); a.setAmount(Money.of(req.amount())); a.setSettledAmount(BigDecimal.ZERO); a.setRemainAmount(Money.of(req.amount())); a.setReceiptDate(req.receiptDate() != null ? req.receiptDate() : LocalDate.now().toString()); a.setStatus(FinAdvance.S_OPEN); a.setRemark(req.remark()); a.setOperator(req.operator()); a.setCreatedAt(Instant.now()); return ApiResp.ok(advRepo.save(a)); } @PutMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody AdvanceRequest req) { FinAdvance a = advRepo.findById(id) .orElseThrow(() -> new NotFoundException("预收/预付记录不存在: " + id)); if (FinAdvance.S_SETTLED.equals(a.getStatus())) { throw new ApiException(409, "已核销完毕的记录不允许修改"); } if (req.partyName() != null && !req.partyName().isBlank()) a.setPartyName(req.partyName()); if (req.contractRef() != null) a.setContractRef(req.contractRef()); if (req.receiptDate() != null) a.setReceiptDate(req.receiptDate()); if (req.remark() != null) a.setRemark(req.remark()); return ApiResp.ok(advRepo.save(a)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { FinAdvance a = advRepo.findById(id) .orElseThrow(() -> new NotFoundException("预收/预付记录不存在: " + id)); if (!FinAdvance.S_OPEN.equals(a.getStatus())) { throw new ApiException(409, "已发生核销的记录不允许删除"); } advRepo.deleteById(id); return ApiResp.ok(null); } // ---------- 核销 ---------- public record SettleRequest(Double settleAmount, Long arApItemId, String operator) { } /** * 核销:将预收/预付款向应收/应付单核销,累加已核销额,状态机:待核销→部分核销→已核销。 */ @PostMapping("/{id}/settle") @Transactional public ApiResp settle(@PathVariable Long id, @RequestBody SettleRequest req) { FinAdvance a = advRepo.findById(id) .orElseThrow(() -> new NotFoundException("预收/预付记录不存在: " + id)); if (FinAdvance.S_SETTLED.equals(a.getStatus())) { throw new ApiException(409, "已全额核销,无需再次核销"); } if (req.settleAmount() == null || req.settleAmount() <= 0) { throw new ApiException(400, "核销金额必须大于 0"); } BigDecimal add = Money.of(req.settleAmount()); BigDecimal remain = Money.nz(a.getRemainAmount()); if (Money.gt(add, remain)) { throw new ApiException(409, "核销金额超过未核销余额(剩余 " + remain + ")"); } BigDecimal newSettled = Money.add(a.getSettledAmount(), add); a.setSettledAmount(newSettled); a.setRemainAmount(Money.sub(a.getAmount(), newSettled)); a.setStatus(newSettled.compareTo(a.getAmount()) >= 0 ? FinAdvance.S_SETTLED : FinAdvance.S_PARTIAL); if (req.arApItemId() != null) a.setArApItemId(req.arApItemId()); return ApiResp.ok(advRepo.save(a)); } // ---------- 自动生成凭证 ---------- public record VoucherRequest(String preparer) { } /** * 自动生成凭证: * 预收 → 借 1002/银行存款 / 贷 2203/预收账款 * 预付 → 借 1123/预付账款 / 贷 1002/银行存款 */ @PostMapping("/{id}/voucher") @Transactional public ApiResp generateVoucher(@PathVariable Long id, @RequestBody(required = false) VoucherRequest req) { FinAdvance a = advRepo.findById(id) .orElseThrow(() -> new NotFoundException("预收/预付记录不存在: " + id)); if (a.getVoucherId() != null) { throw new ApiException(409, "已生成凭证,不可重复生成"); } String preparer = req != null && req.preparer() != null ? req.preparer() : "系统自动"; Voucher v = new Voucher(); boolean isReceived = FinAdvance.T_RECEIVED.equals(a.getAdvanceType()); v.setVoucherNo((isReceived ? "YS-" : "YF-") + (voucherRepo.count() + 1)); v.setVoucherDate(a.getReceiptDate() != null ? a.getReceiptDate() : LocalDate.now().toString()); v.setSummary((isReceived ? "收到预收款" : "支付预付款") + "—" + a.getPartyName() + " " + a.getCode()); v.setDebitAccount(isReceived ? "1002" : "1123"); v.setCreditAccount(isReceived ? "2203" : "1002"); v.setAmount(Money.nz(a.getAmount())); v.setStatus(Voucher.S_DRAFT); v.setPreparer(preparer); v.setReversed(Boolean.FALSE); v.setIsReversal(Boolean.FALSE); v.setSourceType(isReceived ? "fin-advance-received" : "fin-advance-paid"); v.setSourceId(a.getId()); v.setCreatedAt(Instant.now()); voucherRepo.save(v); a.setVoucherId(v.getId()); return ApiResp.ok(advRepo.save(a)); } // ---------- 汇总 ---------- @GetMapping("/summary") public ApiResp> summary() { List all = advRepo.findAll(); BigDecimal recTotal = BigDecimal.ZERO, recSettled = BigDecimal.ZERO, recRemain = BigDecimal.ZERO; BigDecimal paidTotal = BigDecimal.ZERO, paidSettled = BigDecimal.ZERO, paidRemain = BigDecimal.ZERO; for (FinAdvance a : all) { if (FinAdvance.T_RECEIVED.equals(a.getAdvanceType())) { recTotal = Money.add(recTotal, a.getAmount()); recSettled = Money.add(recSettled, a.getSettledAmount()); recRemain = Money.add(recRemain, a.getRemainAmount()); } else { paidTotal = Money.add(paidTotal, a.getAmount()); paidSettled = Money.add(paidSettled, a.getSettledAmount()); paidRemain = Money.add(paidRemain, a.getRemainAmount()); } } Map result = new LinkedHashMap<>(); result.put("receivedTotal", recTotal); result.put("receivedSettled", recSettled); result.put("receivedRemain", recRemain); result.put("paidTotal", paidTotal); result.put("paidSettled", paidSettled); result.put("paidRemain", paidRemain); return ApiResp.ok(result); } }