Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/FinAdvanceController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

230 lines
10 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<FinAdvance>> 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<FinAdvance> 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<FinAdvance> 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<FinAdvance> 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<Void> 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<FinAdvance> 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<FinAdvance> 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<Map<String, Object>> summary() {
List<FinAdvance> 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<String, Object> 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);
}
}