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.FertProcurePlan; import com.kaidi.oa.repository.FertProcurePlanRepository; 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; /** * 生物质肥料制造中心·原料采购/收购计划(需求 §1,补深审计 PARTIAL[med])。 * * 据需求量与在库量算建议采购量(planQty - onHandQty)与预计金额;季节性原料锁收购窗口期; * 供应商准入资料附件登记(检测报告/无害化证明);状态机 草稿→已审批→执行中→已完成。 * 写口受 AuthInterceptor default-deny(ADMIN/APPROVER) 保护,前缀已登记入 FINANCE/SENSITIVE_READ。 */ @RestController @RequestMapping("/api/oa/fert-procure-plans") public class FertProcurePlanController { private final FertProcurePlanRepository repo; public FertProcurePlanController(FertProcurePlanRepository repo) { this.repo = repo; } @GetMapping public ApiResp> list(@RequestParam(required = false) String status, @RequestParam(required = false) Boolean seasonalOnly) { if (Boolean.TRUE.equals(seasonalOnly)) return ApiResp.ok(repo.findBySeasonalTrue()); if (status != null && !status.isBlank()) return ApiResp.ok(repo.findByStatus(status)); return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(find(id)); } public record PlanRequest(String planNo, String period, String materialName, String category, Double planQty, Double onHandQty, Double estUnitPrice, Boolean seasonal, String windowStart, String windowEnd, String supplier, String admitDocs, String owner, String remark) { } @PostMapping public ApiResp create(@RequestBody PlanRequest req) { if (req.materialName() == null || req.materialName().isBlank()) { throw new ApiException(400, "原料名称(materialName) 不能为空"); } FertProcurePlan p = new FertProcurePlan(); p.setPlanNo(req.planNo() == null || req.planNo().isBlank() ? "PP-" + (repo.count() + 1) : req.planNo().trim()); p.setStatus("草稿"); p.setReceivedQty(BigDecimal.ZERO); p.setCreatedAt(Instant.now()); apply(p, req); return ApiResp.ok(repo.save(p)); } @PutMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody PlanRequest req) { FertProcurePlan p = find(id); if ("已完成".equals(p.getStatus()) || "已作废".equals(p.getStatus())) { throw new ApiException(409, "已完成/已作废计划不可编辑"); } apply(p, req); return ApiResp.ok(repo.save(p)); } private void apply(FertProcurePlan p, PlanRequest req) { if (req.materialName() != null && !req.materialName().isBlank()) p.setMaterialName(req.materialName().trim()); p.setPeriod(req.period()); p.setCategory(req.category()); BigDecimal planQty = Money.of(req.planQty()); BigDecimal onHand = Money.of(req.onHandQty()); p.setPlanQty(planQty); p.setOnHandQty(onHand); BigDecimal suggest = planQty.subtract(onHand).max(BigDecimal.ZERO); p.setSuggestQty(suggest); BigDecimal price = Money.of(req.estUnitPrice()); p.setEstUnitPrice(price); p.setEstAmount(Money.of(suggest.multiply(price))); p.setSeasonal(Boolean.TRUE.equals(req.seasonal())); p.setWindowStart(req.windowStart()); p.setWindowEnd(req.windowEnd()); p.setSupplier(req.supplier()); p.setAdmitDocs(req.admitDocs()); p.setOwner(req.owner()); p.setRemark(req.remark()); } public record ApproveRequest(String approver) { } @PostMapping("/{id}/approve") public ApiResp approve(@PathVariable Long id, @RequestBody(required = false) ApproveRequest req) { FertProcurePlan p = find(id); if (!"草稿".equals(p.getStatus())) { throw new ApiException(409, "仅草稿计划可审批,当前:" + p.getStatus()); } p.setStatus("已审批"); p.setApprover(req == null ? null : req.approver()); return ApiResp.ok(repo.save(p)); } @PostMapping("/{id}/start") public ApiResp start(@PathVariable Long id) { FertProcurePlan p = find(id); if (!"已审批".equals(p.getStatus())) { throw new ApiException(409, "仅已审批计划可转执行,当前:" + p.getStatus()); } p.setStatus("执行中"); return ApiResp.ok(repo.save(p)); } public record ReceiveRequest(Double receivedQty) { } /** 登记到货进度;累计到货达建议量自动完成。 */ @PostMapping("/{id}/receive") public ApiResp receive(@PathVariable Long id, @RequestBody ReceiveRequest req) { FertProcurePlan p = find(id); if (!"执行中".equals(p.getStatus()) && !"已审批".equals(p.getStatus())) { throw new ApiException(409, "计划需审批/执行中才能登记到货,当前:" + p.getStatus()); } BigDecimal add = Money.of(req.receivedQty()); if (add.signum() <= 0) { throw new ApiException(400, "到货量必须大于 0"); } p.setReceivedQty(Money.of(p.getReceivedQty().add(add))); if ("已审批".equals(p.getStatus())) p.setStatus("执行中"); if (p.getReceivedQty().compareTo(p.getSuggestQty()) >= 0 && p.getSuggestQty().signum() > 0) { p.setStatus("已完成"); } return ApiResp.ok(repo.save(p)); } @PostMapping("/{id}/cancel") public ApiResp cancel(@PathVariable Long id) { FertProcurePlan p = find(id); if ("已完成".equals(p.getStatus())) { throw new ApiException(409, "已完成计划不可作废"); } p.setStatus("已作废"); return ApiResp.ok(repo.save(p)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { repo.delete(find(id)); return ApiResp.ok(); } private FertProcurePlan find(Long id) { return repo.findById(id).orElseThrow(() -> new NotFoundException("采购计划不存在:" + id)); } }