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])。
*
* 补全缺口:包装物库(编织袋/标签/吨袋)独立台账实体,与成品批次明细关联管理,库位管理。
*
* 端点:
*
* - GET /fert-pkg-material-ledger:列表(可按 status/packSpec/keyword 筛选);
* - POST /fert-pkg-material-ledger:建档(pkgCode 唯一约束 → 409);
* - PUT /fert-pkg-material-ledger/{id}:更新描述字段;
* - DELETE /fert-pkg-material-ledger/{id}:删除;
* - POST /fert-pkg-material-ledger/{id}/inbound:入库(增 qty,记录入库日期);
* - POST /fert-pkg-material-ledger/{id}/issue:领用出库(减 qty,绑定成品批次);
* - POST /fert-pkg-material-ledger/{id}/return:退库;
* - GET /fert-pkg-material-ledger/low-stock:低库存预警(qty <= safetyStock);
* - GET /fert-pkg-material-ledger/summary:各包装规格汇总统计;
*
*/
@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 "在库";
}
}