恢复点(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>
243 lines
11 KiB
Java
243 lines
11 KiB
Java
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<FinVoucherTemplate>> 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<FinVoucherTemplate> 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<FinVoucherTemplate> 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<FinVoucherTemplate> 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<Void> 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<Voucher> 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<List<Voucher>> 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<Voucher> 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<List<String>> 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
|
|
));
|
|
}
|
|
}
|