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])。 * * 补全缺口:包装物库(编织袋/标签/吨袋)独立台账实体,与成品批次明细关联管理,库位管理。 * * 端点: * */ @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( @RequestParam(required = false) String status, @RequestParam(required = false) String packSpec, @RequestParam(required = false) String keyword) { List 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 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 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 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 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 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 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 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> lowStock() { List 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> summary() { java.util.Map 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 out = new ArrayList<>(); for (java.util.Map.Entry 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 "在库"; } }