Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/FertPkgMaterialLedgerController.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

253 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.FertPkgMaterialLedger;
import com.kaidi.oa.repository.FertPkgMaterialLedgerRepository;
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.ArrayList;
import java.util.List;
/**
* 生物质肥料制造中心·包装物独立台账(需求 §6,审计 PARTIAL[low])。
*
* 补全缺口:包装物库(编织袋/标签/吨袋)独立台账实体,与成品批次明细关联管理,库位管理。
*
* 端点:
* <ul>
* <li>GET /fert-pkg-material-ledger:列表(可按 status/packSpec/keyword 筛选);</li>
* <li>POST /fert-pkg-material-ledger:建档(pkgCode 唯一约束 → 409);</li>
* <li>PUT /fert-pkg-material-ledger/{id}:更新描述字段;</li>
* <li>DELETE /fert-pkg-material-ledger/{id}:删除;</li>
* <li>POST /fert-pkg-material-ledger/{id}/inbound:入库(增 qty,记录入库日期);</li>
* <li>POST /fert-pkg-material-ledger/{id}/issue:领用出库(减 qty,绑定成品批次);</li>
* <li>POST /fert-pkg-material-ledger/{id}/return:退库;</li>
* <li>GET /fert-pkg-material-ledger/low-stock:低库存预警(qty &lt;= safetyStock);</li>
* <li>GET /fert-pkg-material-ledger/summary:各包装规格汇总统计;</li>
* </ul>
*/
@RestController
@RequestMapping("/api/oa/fert-pkg-material-ledger")
public class FertPkgMaterialLedgerController {
private final FertPkgMaterialLedgerRepository repo;
public FertPkgMaterialLedgerController(FertPkgMaterialLedgerRepository repo) {
this.repo = repo;
}
// -------- 列表 --------
@GetMapping
public ApiResp<List<FertPkgMaterialLedger>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String packSpec,
@RequestParam(required = false) String keyword) {
List<FertPkgMaterialLedger> all;
if (keyword != null && !keyword.isBlank()) {
all = repo.findByPkgNameContaining(keyword.trim());
} else if (packSpec != null && !packSpec.isBlank()) {
all = repo.findByPackSpec(packSpec.trim());
} else if (status != null && !status.isBlank()) {
all = repo.findByStatus(status.trim());
} else {
all = repo.findAll();
}
return ApiResp.ok(all);
}
@GetMapping("/{id}")
public ApiResp<FertPkgMaterialLedger> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
// -------- 建档/更新/删除 --------
public record PkgRequest(
String pkgCode, String pkgName, String spec, String bin, String unit,
Double qty, Double safetyStock, Double unitCost, String supplier,
String packSpec, String remark) {
}
@PostMapping
@Transactional
public ApiResp<FertPkgMaterialLedger> create(@RequestBody PkgRequest req) {
if (req.pkgName() == null || req.pkgName().isBlank()) {
throw new ApiException(400, "包装物名称(pkgName) 不能为空");
}
String code = (req.pkgCode() == null || req.pkgCode().isBlank())
? "PKG-" + (repo.count() + 1) : req.pkgCode().trim();
if (repo.findByPkgCode(code).isPresent()) {
throw new ApiException(409, "包装物编码已存在:" + code);
}
FertPkgMaterialLedger e = new FertPkgMaterialLedger();
e.setPkgCode(code);
e.setPkgName(req.pkgName().trim());
e.setSpec(req.spec());
e.setWarehouse("包装物库");
e.setBin(req.bin() == null || req.bin().isBlank() ? "A货架-01" : req.bin().trim());
e.setUnit(req.unit() == null || req.unit().isBlank() ? "条" : req.unit().trim());
BigDecimal qty = Money.of(req.qty());
e.setQty(qty);
e.setSafetyStock(Money.of(req.safetyStock()));
e.setUnitCost(Money.of(req.unitCost()));
e.setSupplier(req.supplier());
e.setPackSpec(req.packSpec());
e.setLastInboundDate(LocalDate.now().toString());
e.setLastMoveType(qty.signum() > 0 ? "入库" : null);
e.setStatus(evalStatus(qty, Money.of(req.safetyStock())));
e.setRemark(req.remark());
e.setCreatedAt(Instant.now());
return ApiResp.ok(repo.save(e));
}
@PutMapping("/{id}")
@Transactional
public ApiResp<FertPkgMaterialLedger> update(@PathVariable Long id, @RequestBody PkgRequest req) {
FertPkgMaterialLedger e = find(id);
if (req.pkgName() != null && !req.pkgName().isBlank()) e.setPkgName(req.pkgName().trim());
if (req.spec() != null) e.setSpec(req.spec());
if (req.bin() != null && !req.bin().isBlank()) e.setBin(req.bin().trim());
if (req.unit() != null && !req.unit().isBlank()) e.setUnit(req.unit().trim());
if (req.safetyStock() != null) e.setSafetyStock(Money.of(req.safetyStock()));
if (req.unitCost() != null) e.setUnitCost(Money.of(req.unitCost()));
if (req.supplier() != null) e.setSupplier(req.supplier());
if (req.packSpec() != null) e.setPackSpec(req.packSpec());
if (req.remark() != null) e.setRemark(req.remark());
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
return ApiResp.ok(repo.save(e));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
repo.delete(find(id));
return ApiResp.ok();
}
// -------- 出入库操作 --------
public record MoveRequest(Double qty, String linkedBatchNo, String operator, String remark) {
}
/** 入库:增 qty,记录入库日期。 */
@PostMapping("/{id}/inbound")
@Transactional
public ApiResp<FertPkgMaterialLedger> inbound(@PathVariable Long id, @RequestBody MoveRequest req) {
FertPkgMaterialLedger e = find(id);
BigDecimal q = requirePositive(req.qty());
e.setQty(Money.of(e.getQty().add(q)));
e.setLastInboundDate(LocalDate.now().toString());
e.setLastMoveType("入库");
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
return ApiResp.ok(repo.save(e));
}
/** 领用出库:减 qty,绑定成品批次号(补全与成品明细关联管理缺口)。 */
@PostMapping("/{id}/issue")
@Transactional
public ApiResp<FertPkgMaterialLedger> issue(@PathVariable Long id, @RequestBody MoveRequest req) {
FertPkgMaterialLedger e = find(id);
BigDecimal q = requirePositive(req.qty());
if (q.compareTo(e.getQty()) > 0) {
throw new ApiException(400, "领用数量(" + q + ")超过库存(" + e.getQty() + ")");
}
e.setQty(Money.of(e.getQty().subtract(q)));
if (req.linkedBatchNo() != null && !req.linkedBatchNo().isBlank()) {
e.setLinkedBatchNo(req.linkedBatchNo().trim());
}
e.setLastMoveType("领用");
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
return ApiResp.ok(repo.save(e));
}
/** 退库:增 qty,清除成品批次关联。 */
@PostMapping("/{id}/return")
@Transactional
public ApiResp<FertPkgMaterialLedger> returnToStock(@PathVariable Long id, @RequestBody MoveRequest req) {
FertPkgMaterialLedger e = find(id);
BigDecimal q = requirePositive(req.qty());
e.setQty(Money.of(e.getQty().add(q)));
e.setLastMoveType("退库");
e.setStatus(evalStatus(e.getQty(), e.getSafetyStock()));
return ApiResp.ok(repo.save(e));
}
// -------- 低库存预警 --------
public record LowStockRow(Long id, String pkgCode, String pkgName, String spec, String packSpec,
BigDecimal qty, BigDecimal safetyStock, String unit, String bin) {
}
@GetMapping("/low-stock")
public ApiResp<List<LowStockRow>> lowStock() {
List<LowStockRow> out = new ArrayList<>();
for (FertPkgMaterialLedger e : repo.findAll()) {
if (e.getSafetyStock() != null && e.getSafetyStock().signum() > 0
&& e.getQty().compareTo(e.getSafetyStock()) <= 0) {
out.add(new LowStockRow(e.getId(), e.getPkgCode(), e.getPkgName(),
e.getSpec(), e.getPackSpec(), e.getQty(), e.getSafetyStock(),
e.getUnit(), e.getBin()));
}
}
return ApiResp.ok(out);
}
// -------- 规格汇总统计 --------
public record PkgSummaryRow(String packSpec, BigDecimal totalQty, int itemCount, BigDecimal totalValue) {
}
@GetMapping("/summary")
public ApiResp<List<PkgSummaryRow>> summary() {
java.util.Map<String, BigDecimal[]> map = new java.util.LinkedHashMap<>();
for (FertPkgMaterialLedger e : repo.findAll()) {
String key = e.getPackSpec() == null ? "未指定" : e.getPackSpec();
BigDecimal[] arr = map.computeIfAbsent(key, k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO});
arr[0] = arr[0].add(e.getQty());
arr[1] = arr[1].add(BigDecimal.ONE);
BigDecimal val = e.getQty().multiply(e.getUnitCost() == null ? BigDecimal.ZERO : e.getUnitCost());
arr[2] = arr[2].add(val);
}
List<PkgSummaryRow> out = new ArrayList<>();
for (java.util.Map.Entry<String, BigDecimal[]> entry : map.entrySet()) {
out.add(new PkgSummaryRow(entry.getKey(), Money.of(entry.getValue()[0]),
entry.getValue()[1].intValue(), Money.of(entry.getValue()[2])));
}
return ApiResp.ok(out);
}
// -------- helpers --------
private FertPkgMaterialLedger find(Long id) {
return repo.findById(id).orElseThrow(() -> new NotFoundException("包装物台账记录不存在:" + id));
}
private static BigDecimal requirePositive(Double v) {
BigDecimal q = Money.of(v);
if (q.signum() <= 0) throw new ApiException(400, "数量必须大于 0");
return q;
}
private static String evalStatus(BigDecimal qty, BigDecimal safety) {
if (qty == null || qty.signum() <= 0) return "已耗尽";
if (safety != null && safety.signum() > 0 && qty.compareTo(safety) <= 0) return "低库存";
return "在库";
}
}