feat(erp): 采购计划/采购审核/到货跟踪 + 供应商管理
- 采购计划:选合同自动带出明细、合同/供应商表格选择器、批量填充(可生成N行)、卷号/数量列、送审/重新送审流程 - 采购审核:通过/驳回 + 申请意见,每次审核留痕(erp_purchase_plan_audit_log),计划详情展示审核历史/驳回理由 - 到货跟踪:上传到货Excel按牌号+规格回填明细到货量与状态,列校验/kg→t纠正,满额自动归档 - 供应商管理页(复用既有 erp_supplier 后端) - 综合搜索(计划号/供货商/合同号)、左右分栏工作台、全局表单按钮对齐修复 - 清理无用旧 erp 页面(看板/需求/订单/收货/退货/汇总) - DDL 与菜单脚本:docs/purchase-plan-ddl.sql(按 path 解析父目录、可重复执行) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package com.klp.erp.controller;
|
||||
|
||||
import com.klp.common.annotation.Log;
|
||||
import com.klp.common.annotation.RepeatSubmit;
|
||||
import com.klp.common.core.controller.BaseController;
|
||||
import com.klp.common.core.domain.PageQuery;
|
||||
import com.klp.common.core.domain.R;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.common.core.validate.AddGroup;
|
||||
import com.klp.common.core.validate.EditGroup;
|
||||
import com.klp.common.enums.BusinessType;
|
||||
import com.klp.common.utils.poi.ExcelUtil;
|
||||
import com.klp.erp.domain.bo.ErpPurchasePlanAuditBo;
|
||||
import com.klp.erp.domain.bo.ErpPurchasePlanBo;
|
||||
import com.klp.erp.domain.vo.ErpContractOptionVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
|
||||
import com.klp.erp.service.IErpPurchasePlanService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 采购计划
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/erp/purchasePlan")
|
||||
public class ErpPurchasePlanController extends BaseController {
|
||||
|
||||
private final IErpPurchasePlanService iErpPurchasePlanService;
|
||||
|
||||
/** 查询采购计划列表 */
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<ErpPurchasePlanVo> list(ErpPurchasePlanBo bo, PageQuery pageQuery) {
|
||||
return iErpPurchasePlanService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/** 总体到货进度统计(已完成 N / 共 M) */
|
||||
@GetMapping("/statistics")
|
||||
public R<Map<String, Object>> statistics(ErpPurchasePlanBo bo) {
|
||||
return R.ok(iErpPurchasePlanService.statistics(bo));
|
||||
}
|
||||
|
||||
/** 导出采购计划列表 */
|
||||
@Log(title = "采购计划", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(ErpPurchasePlanBo bo, HttpServletResponse response) {
|
||||
List<ErpPurchasePlanVo> list = iErpPurchasePlanService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "采购计划", ErpPurchasePlanVo.class, response);
|
||||
}
|
||||
|
||||
/** 获取采购计划详细信息 */
|
||||
@GetMapping("/{planId}")
|
||||
public R<ErpPurchasePlanVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long planId) {
|
||||
return R.ok(iErpPurchasePlanService.queryById(planId));
|
||||
}
|
||||
|
||||
/** 按销售合同取明细(选合同自动带出明细:1/2/3合同 -> 1/2/3/4明细) */
|
||||
@GetMapping("/itemsByOrders")
|
||||
public R<List<ErpPurchasePlanItemVo>> itemsByOrders(@RequestParam("orderIds") List<Long> orderIds) {
|
||||
return R.ok(iErpPurchasePlanService.queryItemsByOrders(orderIds));
|
||||
}
|
||||
|
||||
/** 合同列表(左侧):crm_order + 每个合同已有的采购计划数 */
|
||||
@GetMapping("/contracts")
|
||||
public TableDataInfo<ErpContractOptionVo> contracts(@RequestParam(value = "keyword", required = false) String keyword,
|
||||
PageQuery pageQuery) {
|
||||
return iErpPurchasePlanService.queryContractPage(keyword, pageQuery);
|
||||
}
|
||||
|
||||
/** 某合同下的所有采购计划 */
|
||||
@GetMapping("/byContract/{orderId}")
|
||||
public R<List<ErpPurchasePlanVo>> byContract(@PathVariable Long orderId) {
|
||||
return R.ok(iErpPurchasePlanService.queryPlansByContract(orderId));
|
||||
}
|
||||
|
||||
/** 新增采购计划 */
|
||||
@Log(title = "采购计划", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody ErpPurchasePlanBo bo) {
|
||||
return toAjax(iErpPurchasePlanService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/** 修改采购计划 */
|
||||
@Log(title = "采购计划", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody ErpPurchasePlanBo bo) {
|
||||
return toAjax(iErpPurchasePlanService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/** 审核(通过/驳回 + 申请意见) */
|
||||
@Log(title = "采购计划", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/audit")
|
||||
public R<Void> audit(@Validated @RequestBody ErpPurchasePlanAuditBo bo) {
|
||||
return toAjax(iErpPurchasePlanService.audit(bo, getUsername()));
|
||||
}
|
||||
|
||||
/** 送审 / 重新送审 */
|
||||
@Log(title = "采购计划", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/submit/{planId}")
|
||||
public R<Void> submit(@NotNull(message = "主键不能为空") @PathVariable Long planId) {
|
||||
return toAjax(iErpPurchasePlanService.submitForAudit(planId));
|
||||
}
|
||||
|
||||
/** 删除采购计划 */
|
||||
@Log(title = "采购计划", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{planIds}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] planIds) {
|
||||
return toAjax(iErpPurchasePlanService.deleteWithValidByIds(Arrays.asList(planIds), true));
|
||||
}
|
||||
|
||||
/** 导入到货 Excel */
|
||||
@Log(title = "采购计划-到货", businessType = BusinessType.IMPORT)
|
||||
@PostMapping(value = "/{planId}/importDelivery", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<Map<String, Object>> importDelivery(@PathVariable Long planId,
|
||||
@RequestPart("file") MultipartFile file) throws Exception {
|
||||
Map<String, Object> result = iErpPurchasePlanService.importDelivery(planId, file.getInputStream(), getUsername());
|
||||
return R.ok(String.valueOf(result.get("message")), result);
|
||||
}
|
||||
|
||||
/** 查询某计划的到货明细 */
|
||||
@GetMapping("/{planId}/delivery")
|
||||
public R<List<ErpPurchasePlanDeliveryVo>> deliveryList(@PathVariable Long planId) {
|
||||
return R.ok(iErpPurchasePlanService.queryDeliveryList(planId));
|
||||
}
|
||||
|
||||
/** 删除到货明细 */
|
||||
@Log(title = "采购计划-到货", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/delivery/{deliveryId}")
|
||||
public R<Void> deleteDelivery(@PathVariable Long deliveryId) {
|
||||
return toAjax(iErpPurchasePlanService.deleteDelivery(deliveryId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.klp.erp.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.klp.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 采购计划头对象 erp_purchase_plan
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("erp_purchase_plan")
|
||||
public class ErpPurchasePlan extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 计划ID */
|
||||
@TableId(value = "plan_id")
|
||||
private Long planId;
|
||||
|
||||
/** 采购计划号 */
|
||||
private String planNo;
|
||||
|
||||
/** 计划状态: 0-进行中 1-已完成归档 */
|
||||
private String planStatus;
|
||||
|
||||
/** 审核状态: 0-待审核 1-通过 2-驳回 */
|
||||
private String auditStatus;
|
||||
|
||||
/** 申请/审核意见 */
|
||||
private String auditOpinion;
|
||||
|
||||
/** 审核人 */
|
||||
private String auditor;
|
||||
|
||||
/** 审核时间 */
|
||||
private Date auditTime;
|
||||
|
||||
/** 供货商 */
|
||||
private String supplier;
|
||||
|
||||
/** 采购日期 */
|
||||
private Date purchaseDate;
|
||||
|
||||
/** 计划总重量(T) */
|
||||
private BigDecimal planWeight;
|
||||
|
||||
/** 已到货重量(T) */
|
||||
private BigDecimal arrivedWeight;
|
||||
|
||||
/** 删除标志 */
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.klp.erp.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 采购计划审核日志(每次审核留痕)
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-25
|
||||
*/
|
||||
@Data
|
||||
@TableName("erp_purchase_plan_audit_log")
|
||||
public class ErpPurchasePlanAuditLog implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "log_id")
|
||||
private Long logId;
|
||||
|
||||
private Long planId;
|
||||
|
||||
/** 审核结果: 1-通过 2-驳回 */
|
||||
private String auditStatus;
|
||||
|
||||
/** 审核/驳回意见 */
|
||||
private String auditOpinion;
|
||||
|
||||
private String auditor;
|
||||
|
||||
private Date auditTime;
|
||||
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.klp.erp.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.klp.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 采购计划-销售合同关联对象 erp_purchase_plan_contract_rel
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("erp_purchase_plan_contract_rel")
|
||||
public class ErpPurchasePlanContractRel extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 关系ID */
|
||||
@TableId(value = "rel_id")
|
||||
private Long relId;
|
||||
|
||||
/** 采购计划ID */
|
||||
private Long planId;
|
||||
|
||||
/** 销售合同ID(crm_order.order_id) */
|
||||
private Long orderId;
|
||||
|
||||
/** 删除标志 */
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.klp.erp.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.klp.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 采购计划到货明细对象 erp_purchase_plan_delivery
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("erp_purchase_plan_delivery")
|
||||
public class ErpPurchasePlanDelivery extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 到货明细ID */
|
||||
@TableId(value = "delivery_id")
|
||||
private Long deliveryId;
|
||||
|
||||
/** 关联计划ID */
|
||||
private Long planId;
|
||||
|
||||
/** 日期 */
|
||||
private Date arrivalDate;
|
||||
|
||||
/** 牌号 */
|
||||
private String grade;
|
||||
|
||||
/** 规格(厚×宽) */
|
||||
private String spec;
|
||||
|
||||
/** 卷号 */
|
||||
private String coilNo;
|
||||
|
||||
/** 单卷重量(T) */
|
||||
private BigDecimal coilWeight;
|
||||
|
||||
/** 车号 */
|
||||
private String truckNo;
|
||||
|
||||
/** 整车数量(T) */
|
||||
private BigDecimal truckWeight;
|
||||
|
||||
/** 件数 */
|
||||
private Integer pieceCount;
|
||||
|
||||
/** 销售代码 */
|
||||
private String salesCode;
|
||||
|
||||
/** 钢厂到站 */
|
||||
private String arrivalStation;
|
||||
|
||||
/** 删除标志 */
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.klp.erp.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.klp.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 采购计划明细对象 erp_purchase_plan_item
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("erp_purchase_plan_item")
|
||||
public class ErpPurchasePlanItem extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 明细ID */
|
||||
@TableId(value = "item_id")
|
||||
private Long itemId;
|
||||
|
||||
/** 关联计划ID */
|
||||
private Long planId;
|
||||
|
||||
/** 产品(如热轧卷板) */
|
||||
private String productType;
|
||||
|
||||
/** 材质 */
|
||||
private String material;
|
||||
|
||||
/** 牌号 */
|
||||
private String grade;
|
||||
|
||||
/** 卷号 */
|
||||
private String coilNo;
|
||||
|
||||
/** 宽度(mm,可为区间文本) */
|
||||
private String width;
|
||||
|
||||
/** 厚度(mm,可为区间文本) */
|
||||
private String thickness;
|
||||
|
||||
/** 宽度公差 */
|
||||
private String widthTolerance;
|
||||
|
||||
/** 厚度公差 */
|
||||
private String thicknessTolerance;
|
||||
|
||||
/** 重量(T) */
|
||||
private BigDecimal weight;
|
||||
|
||||
/** 数量(件/卷数) */
|
||||
private Integer quantity;
|
||||
|
||||
/** 已到货重量(T),由到货Excel按牌号+规格累加 */
|
||||
private BigDecimal arrivedWeight;
|
||||
|
||||
/** 到货状态: 0-未到货 1-部分到货 2-已到货 */
|
||||
private String itemStatus;
|
||||
|
||||
/** 供货商 */
|
||||
private String supplier;
|
||||
|
||||
/** 删除标志 */
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.klp.erp.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 采购计划审核业务对象
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
public class ErpPurchasePlanAuditBo {
|
||||
|
||||
/** 计划ID */
|
||||
@NotNull(message = "计划ID不能为空")
|
||||
private Long planId;
|
||||
|
||||
/** 审核结果: 1-通过 2-驳回 */
|
||||
@NotNull(message = "审核结果不能为空")
|
||||
private String auditStatus;
|
||||
|
||||
/** 申请/审核意见 */
|
||||
private String auditOpinion;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.klp.erp.domain.bo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.klp.common.core.domain.BaseEntity;
|
||||
import com.klp.common.core.validate.AddGroup;
|
||||
import com.klp.common.core.validate.EditGroup;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 采购计划头业务对象 erp_purchase_plan
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ErpPurchasePlanBo extends BaseEntity {
|
||||
|
||||
/** 计划ID */
|
||||
@NotNull(message = "计划ID不能为空", groups = {EditGroup.class})
|
||||
private Long planId;
|
||||
|
||||
/** 采购计划号(为空时自动生成) */
|
||||
private String planNo;
|
||||
|
||||
/** 综合搜索关键字:计划号 / 供货商 / 合同号 */
|
||||
private String keyword;
|
||||
|
||||
/** 计划状态: 0-进行中 1-已完成归档 */
|
||||
private String planStatus;
|
||||
|
||||
/** 审核状态: 0-待审核 1-通过 2-驳回 */
|
||||
private String auditStatus;
|
||||
|
||||
/** 供货商 */
|
||||
private String supplier;
|
||||
|
||||
/** 采购日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date purchaseDate;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 明细行 */
|
||||
private List<ErpPurchasePlanItemBo> items;
|
||||
|
||||
/** 关联的销售合同ID列表(crm_order.order_id) */
|
||||
private List<Long> orderIds;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.klp.erp.domain.bo;
|
||||
|
||||
import com.klp.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 采购计划明细业务对象 erp_purchase_plan_item
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ErpPurchasePlanItemBo extends BaseEntity {
|
||||
|
||||
/** 明细ID */
|
||||
private Long itemId;
|
||||
|
||||
/** 关联计划ID */
|
||||
private Long planId;
|
||||
|
||||
/** 产品(如热轧卷板) */
|
||||
private String productType;
|
||||
|
||||
/** 材质 */
|
||||
private String material;
|
||||
|
||||
/** 牌号 */
|
||||
private String grade;
|
||||
|
||||
/** 卷号 */
|
||||
private String coilNo;
|
||||
|
||||
/** 宽度(mm,可为区间文本) */
|
||||
private String width;
|
||||
|
||||
/** 厚度(mm,可为区间文本) */
|
||||
private String thickness;
|
||||
|
||||
/** 宽度公差 */
|
||||
private String widthTolerance;
|
||||
|
||||
/** 厚度公差 */
|
||||
private String thicknessTolerance;
|
||||
|
||||
/** 重量(T) */
|
||||
private BigDecimal weight;
|
||||
|
||||
/** 数量(件/卷数) */
|
||||
private Integer quantity;
|
||||
|
||||
/** 供货商 */
|
||||
private String supplier;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.klp.erp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 采购计划左侧「合同列表」视图对象:crm_order + 该合同已有的采购计划数。
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-25
|
||||
*/
|
||||
@Data
|
||||
public class ErpContractOptionVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 销售合同ID(crm_order.order_id) */
|
||||
private Long orderId;
|
||||
|
||||
/** 订单编号 */
|
||||
private String orderCode;
|
||||
|
||||
/** 合同号 */
|
||||
private String contractCode;
|
||||
|
||||
/** 合同名称 */
|
||||
private String contractName;
|
||||
|
||||
/** 需方(客户) */
|
||||
private String customer;
|
||||
|
||||
/** 供方 */
|
||||
private String supplier;
|
||||
|
||||
/** 订单总金额 */
|
||||
private BigDecimal orderAmount;
|
||||
|
||||
/** 销售员 */
|
||||
private String salesman;
|
||||
|
||||
/** 该合同已挂接的采购计划数 */
|
||||
private Integer planCount;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.klp.erp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 采购计划审核日志视图对象
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-25
|
||||
*/
|
||||
@Data
|
||||
public class ErpPurchasePlanAuditLogVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long logId;
|
||||
|
||||
private Long planId;
|
||||
|
||||
/** 审核结果: 1-通过 2-驳回 */
|
||||
private String auditStatus;
|
||||
|
||||
private String auditOpinion;
|
||||
|
||||
private String auditor;
|
||||
|
||||
private Date auditTime;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.klp.erp.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 采购计划到货导入模板(对应到货 Excel,一行一卷)
|
||||
*
|
||||
* 列:日期 牌号 规格 卷号 单卷重量 车号 数量 件数 销售 钢厂到站
|
||||
* 注:车号/数量/件数 为合并单元格,仅首行有值,导入时向下填充。
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class ErpPurchasePlanDeliveryImportVo {
|
||||
|
||||
@ExcelProperty("日期")
|
||||
private String arrivalDate;
|
||||
|
||||
@ExcelProperty("牌号")
|
||||
private String grade;
|
||||
|
||||
@ExcelProperty("规格")
|
||||
private String spec;
|
||||
|
||||
@ExcelProperty("卷号")
|
||||
private String coilNo;
|
||||
|
||||
@ExcelProperty("单卷重量")
|
||||
private BigDecimal coilWeight;
|
||||
|
||||
@ExcelProperty("车号")
|
||||
private String truckNo;
|
||||
|
||||
@ExcelProperty("数量")
|
||||
private BigDecimal truckWeight;
|
||||
|
||||
@ExcelProperty("件数")
|
||||
private Integer pieceCount;
|
||||
|
||||
@ExcelProperty("销售")
|
||||
private String salesCode;
|
||||
|
||||
@ExcelProperty("钢厂到站")
|
||||
private String arrivalStation;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.klp.erp.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 采购计划到货明细视图对象 erp_purchase_plan_delivery
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class ErpPurchasePlanDeliveryVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty(value = "到货明细ID")
|
||||
private Long deliveryId;
|
||||
|
||||
private Long planId;
|
||||
|
||||
@ExcelProperty(value = "日期")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date arrivalDate;
|
||||
|
||||
@ExcelProperty(value = "牌号")
|
||||
private String grade;
|
||||
|
||||
@ExcelProperty(value = "规格")
|
||||
private String spec;
|
||||
|
||||
@ExcelProperty(value = "卷号")
|
||||
private String coilNo;
|
||||
|
||||
@ExcelProperty(value = "单卷重量")
|
||||
private BigDecimal coilWeight;
|
||||
|
||||
@ExcelProperty(value = "车号")
|
||||
private String truckNo;
|
||||
|
||||
@ExcelProperty(value = "数量")
|
||||
private BigDecimal truckWeight;
|
||||
|
||||
@ExcelProperty(value = "件数")
|
||||
private Integer pieceCount;
|
||||
|
||||
@ExcelProperty(value = "销售")
|
||||
private String salesCode;
|
||||
|
||||
@ExcelProperty(value = "钢厂到站")
|
||||
private String arrivalStation;
|
||||
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.klp.erp.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 采购计划明细视图对象 erp_purchase_plan_item
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class ErpPurchasePlanItemVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty(value = "明细ID")
|
||||
private Long itemId;
|
||||
|
||||
private Long planId;
|
||||
|
||||
@ExcelProperty(value = "产品")
|
||||
private String productType;
|
||||
|
||||
@ExcelProperty(value = "材质")
|
||||
private String material;
|
||||
|
||||
@ExcelProperty(value = "牌号")
|
||||
private String grade;
|
||||
|
||||
@ExcelProperty(value = "卷号")
|
||||
private String coilNo;
|
||||
|
||||
@ExcelProperty(value = "宽度")
|
||||
private String width;
|
||||
|
||||
@ExcelProperty(value = "厚度")
|
||||
private String thickness;
|
||||
|
||||
@ExcelProperty(value = "宽度公差")
|
||||
private String widthTolerance;
|
||||
|
||||
@ExcelProperty(value = "厚度公差")
|
||||
private String thicknessTolerance;
|
||||
|
||||
@ExcelProperty(value = "重量(T)")
|
||||
private BigDecimal weight;
|
||||
|
||||
@ExcelProperty(value = "数量")
|
||||
private Integer quantity;
|
||||
|
||||
@ExcelProperty(value = "已到货(T)")
|
||||
private BigDecimal arrivedWeight;
|
||||
|
||||
/** 到货状态: 0未到货 1部分到货 2已到货 */
|
||||
private String itemStatus;
|
||||
|
||||
@ExcelProperty(value = "供货商")
|
||||
private String supplier;
|
||||
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.klp.erp.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 采购计划头视图对象 erp_purchase_plan
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class ErpPurchasePlanVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty(value = "计划ID")
|
||||
private Long planId;
|
||||
|
||||
@ExcelProperty(value = "采购计划号")
|
||||
private String planNo;
|
||||
|
||||
/** 计划状态: 0-进行中 1-已完成归档 */
|
||||
@ExcelProperty(value = "计划状态")
|
||||
private String planStatus;
|
||||
|
||||
/** 审核状态: 0-待审核 1-通过 2-驳回 */
|
||||
@ExcelProperty(value = "审核状态")
|
||||
private String auditStatus;
|
||||
|
||||
@ExcelProperty(value = "审核意见")
|
||||
private String auditOpinion;
|
||||
|
||||
@ExcelProperty(value = "审核人")
|
||||
private String auditor;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@ExcelProperty(value = "审核时间")
|
||||
private Date auditTime;
|
||||
|
||||
@ExcelProperty(value = "供货商")
|
||||
private String supplier;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@ExcelProperty(value = "采购日期")
|
||||
private Date purchaseDate;
|
||||
|
||||
@ExcelProperty(value = "计划总重量(T)")
|
||||
private BigDecimal planWeight;
|
||||
|
||||
@ExcelProperty(value = "已到货重量(T)")
|
||||
private BigDecimal arrivedWeight;
|
||||
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/** 到货进度百分比(0-100) */
|
||||
private BigDecimal progress;
|
||||
|
||||
/** 明细行 */
|
||||
private List<ErpPurchasePlanItemVo> items;
|
||||
|
||||
/** 关联销售合同ID */
|
||||
private List<Long> orderIds;
|
||||
|
||||
/** 关联销售合同编号(展示用) */
|
||||
private List<String> contractCodes;
|
||||
|
||||
/** 审核历史(每次审核/驳回留痕) */
|
||||
private List<ErpPurchasePlanAuditLogVo> auditLogs;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.klp.erp.listener;
|
||||
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
import com.alibaba.excel.exception.ExcelDataConvertException;
|
||||
import com.klp.common.exception.ServiceException;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryImportVo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 到货 Excel 导入监听器
|
||||
* <p>
|
||||
* 负责:表头/列名校验、逐行数值转换异常收集(不中断),供 Service 统一反馈。
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
public class ErpDeliveryExcelListener extends AnalysisEventListener<ErpPurchasePlanDeliveryImportVo> {
|
||||
|
||||
/** 必需列(与到货模板表头一致) */
|
||||
private static final List<String> REQUIRED_HEADERS = Arrays.asList("日期", "牌号", "规格", "卷号", "单卷重量");
|
||||
|
||||
private final List<ErpPurchasePlanDeliveryImportVo> list = new ArrayList<>();
|
||||
private final List<String> errors = new ArrayList<>();
|
||||
private Map<Integer, String> headMap;
|
||||
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
|
||||
this.headMap = headMap;
|
||||
List<String> headers = headMap.values().stream()
|
||||
.filter(h -> h != null)
|
||||
.map(String::trim)
|
||||
.collect(Collectors.toList());
|
||||
List<String> missing = REQUIRED_HEADERS.stream()
|
||||
.filter(req -> !headers.contains(req))
|
||||
.collect(Collectors.toList());
|
||||
if (!missing.isEmpty()) {
|
||||
throw new ServiceException("到货文件列不匹配,缺少必需列:" + String.join("、", missing)
|
||||
+ "。请使用标准到货模板(日期/牌号/规格/卷号/单卷重量/车号/数量/件数/销售/钢厂到站)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(ErpPurchasePlanDeliveryImportVo data, AnalysisContext context) {
|
||||
list.add(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception exception, AnalysisContext context) throws Exception {
|
||||
// 列校验等业务异常直接上抛,保持原始友好文案
|
||||
if (exception instanceof ServiceException) {
|
||||
throw exception;
|
||||
}
|
||||
if (exception instanceof ExcelDataConvertException) {
|
||||
ExcelDataConvertException e = (ExcelDataConvertException) exception;
|
||||
String head = headMap != null ? headMap.get(e.getColumnIndex()) : null;
|
||||
String headName = head != null ? head : ("第" + (e.getColumnIndex() + 1) + "列");
|
||||
errors.add("第" + (e.getRowIndex() + 1) + "行「" + headName + "」列的值无法识别(应为数字)");
|
||||
} else {
|
||||
errors.add("第" + (context.readRowHolder().getRowIndex() + 1) + "行解析失败:" + exception.getMessage());
|
||||
}
|
||||
// 转换类异常不抛出,继续解析以收集所有错误行
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public List<ErpPurchasePlanDeliveryImportVo> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.klp.erp.mapper;
|
||||
|
||||
import com.klp.common.core.mapper.BaseMapperPlus;
|
||||
import com.klp.erp.domain.ErpPurchasePlanAuditLog;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanAuditLogVo;
|
||||
|
||||
/**
|
||||
* 采购计划审核日志 Mapper
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-25
|
||||
*/
|
||||
public interface ErpPurchasePlanAuditLogMapper extends BaseMapperPlus<ErpPurchasePlanAuditLogMapper, ErpPurchasePlanAuditLog, ErpPurchasePlanAuditLogVo> {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.klp.erp.mapper;
|
||||
|
||||
import com.klp.common.core.mapper.BaseMapperPlus;
|
||||
import com.klp.erp.domain.ErpPurchasePlanContractRel;
|
||||
|
||||
/**
|
||||
* 采购计划-销售合同关联Mapper接口
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
public interface ErpPurchasePlanContractRelMapper extends BaseMapperPlus<ErpPurchasePlanContractRelMapper, ErpPurchasePlanContractRel, ErpPurchasePlanContractRel> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.klp.erp.mapper;
|
||||
|
||||
import com.klp.common.core.mapper.BaseMapperPlus;
|
||||
import com.klp.erp.domain.ErpPurchasePlanDelivery;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryVo;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 采购计划到货明细Mapper接口
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
public interface ErpPurchasePlanDeliveryMapper extends BaseMapperPlus<ErpPurchasePlanDeliveryMapper, ErpPurchasePlanDelivery, ErpPurchasePlanDeliveryVo> {
|
||||
|
||||
/**
|
||||
* 汇总某计划下的已到货重量(Σ单卷重量)
|
||||
*/
|
||||
BigDecimal sumCoilWeightByPlan(@Param("planId") Long planId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.klp.erp.mapper;
|
||||
|
||||
import com.klp.common.core.mapper.BaseMapperPlus;
|
||||
import com.klp.erp.domain.ErpPurchasePlanItem;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
|
||||
|
||||
/**
|
||||
* 采购计划明细Mapper接口
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
public interface ErpPurchasePlanItemMapper extends BaseMapperPlus<ErpPurchasePlanItemMapper, ErpPurchasePlanItem, ErpPurchasePlanItemVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.klp.erp.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.klp.common.core.mapper.BaseMapperPlus;
|
||||
import com.klp.erp.domain.ErpPurchasePlan;
|
||||
import com.klp.erp.domain.vo.ErpContractOptionVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 采购计划头Mapper接口
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
public interface ErpPurchasePlanMapper extends BaseMapperPlus<ErpPurchasePlanMapper, ErpPurchasePlan, ErpPurchasePlanVo> {
|
||||
|
||||
/**
|
||||
* 根据销售合同ID查询合同编号(crm_order,同库跨表)
|
||||
*/
|
||||
List<String> selectOrderCodes(@Param("ids") List<Long> ids);
|
||||
|
||||
/**
|
||||
* 按销售合同ID批量取明细,映射为采购计划明细(来自 crm_order_item)。
|
||||
* 用于「选合同自动带出明细」:1/2/3合同 -> 1/2/3/4明细。
|
||||
*/
|
||||
List<ErpPurchasePlanItemVo> selectItemsByOrderIds(@Param("ids") List<Long> ids);
|
||||
|
||||
/**
|
||||
* 合同列表分页:crm_order + 该合同已挂接的采购计划数。
|
||||
*/
|
||||
Page<ErpContractOptionVo> selectContractPage(IPage<ErpContractOptionVo> page, @Param("kw") String kw);
|
||||
|
||||
/**
|
||||
* 某合同下的所有采购计划(经中间表挂接)。
|
||||
*/
|
||||
List<ErpPurchasePlanVo> selectPlansByContract(@Param("orderId") Long orderId);
|
||||
|
||||
/**
|
||||
* 按合同关键字(订单编号/合同号/合同名称)查出关联的采购计划ID,用于综合搜索。
|
||||
*/
|
||||
List<Long> selectPlanIdsByContractKeyword(@Param("kw") String kw);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.klp.erp.service;
|
||||
|
||||
import com.klp.common.core.domain.PageQuery;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.erp.domain.bo.ErpPurchasePlanAuditBo;
|
||||
import com.klp.erp.domain.bo.ErpPurchasePlanBo;
|
||||
import com.klp.erp.domain.vo.ErpContractOptionVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanAuditLogVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 采购计划Service接口
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
public interface IErpPurchasePlanService {
|
||||
|
||||
/** 查询采购计划详情(含明细、关联合同、进度) */
|
||||
ErpPurchasePlanVo queryById(Long planId);
|
||||
|
||||
/** 按销售合同ID批量取明细(来自 crm_order_item),用于「选合同自动带出明细」 */
|
||||
List<ErpPurchasePlanItemVo> queryItemsByOrders(List<Long> orderIds);
|
||||
|
||||
/** 合同列表分页(含每个合同已有的采购计划数),用于采购计划页左侧 */
|
||||
TableDataInfo<ErpContractOptionVo> queryContractPage(String keyword, PageQuery pageQuery);
|
||||
|
||||
/** 某合同下的所有采购计划 */
|
||||
List<ErpPurchasePlanVo> queryPlansByContract(Long orderId);
|
||||
|
||||
/** 分页查询采购计划 */
|
||||
TableDataInfo<ErpPurchasePlanVo> queryPageList(ErpPurchasePlanBo bo, PageQuery pageQuery);
|
||||
|
||||
/** 查询采购计划列表 */
|
||||
List<ErpPurchasePlanVo> queryList(ErpPurchasePlanBo bo);
|
||||
|
||||
/** 新增采购计划(含明细、合同挂接) */
|
||||
Boolean insertByBo(ErpPurchasePlanBo bo);
|
||||
|
||||
/** 修改采购计划(含明细、合同挂接) */
|
||||
Boolean updateByBo(ErpPurchasePlanBo bo);
|
||||
|
||||
/** 校验并批量删除采购计划 */
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/** 审核(通过/驳回 + 申请意见),每次审核留痕 */
|
||||
Boolean audit(ErpPurchasePlanAuditBo bo, String operator);
|
||||
|
||||
/** 送审 / 重新送审:待送审(3) 或 已驳回(2) → 待审核(0),之后才进入审核页 */
|
||||
Boolean submitForAudit(Long planId);
|
||||
|
||||
/** 某计划的审核历史 */
|
||||
List<ErpPurchasePlanAuditLogVo> queryAuditLogs(Long planId);
|
||||
|
||||
/**
|
||||
* 导入到货 Excel:校验列/数值、kg→t 单位纠正,写入并刷新进度归档。
|
||||
* 返回 {count: 入库条数, message: 回执文案, kgConverted: 是否做了单位换算}
|
||||
*/
|
||||
Map<String, Object> importDelivery(Long planId, InputStream inputStream, String operator);
|
||||
|
||||
/** 查询某计划的到货明细 */
|
||||
List<ErpPurchasePlanDeliveryVo> queryDeliveryList(Long planId);
|
||||
|
||||
/** 删除到货明细,并刷新进度 */
|
||||
Boolean deleteDelivery(Long deliveryId);
|
||||
|
||||
/** 刷新到货进度:arrivedWeight = Σ单卷重量;满 100% 自动归档 */
|
||||
void refreshProgress(Long planId);
|
||||
|
||||
/** 总体到货进度统计(已完成 N / 共 M) */
|
||||
Map<String, Object> statistics(ErpPurchasePlanBo bo);
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
package com.klp.erp.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.klp.common.core.domain.PageQuery;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.common.exception.ServiceException;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.erp.domain.ErpPurchasePlan;
|
||||
import com.klp.erp.domain.ErpPurchasePlanContractRel;
|
||||
import com.klp.erp.domain.ErpPurchasePlanDelivery;
|
||||
import com.klp.erp.domain.ErpPurchasePlanItem;
|
||||
import com.klp.erp.domain.ErpPurchasePlanAuditLog;
|
||||
import com.klp.erp.domain.bo.ErpPurchasePlanAuditBo;
|
||||
import com.klp.erp.domain.bo.ErpPurchasePlanBo;
|
||||
import com.klp.erp.domain.bo.ErpPurchasePlanItemBo;
|
||||
import com.klp.erp.domain.vo.ErpContractOptionVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanAuditLogVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryImportVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
|
||||
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
|
||||
import com.klp.erp.mapper.ErpPurchasePlanContractRelMapper;
|
||||
import com.klp.erp.mapper.ErpPurchasePlanDeliveryMapper;
|
||||
import com.klp.erp.mapper.ErpPurchasePlanItemMapper;
|
||||
import com.klp.erp.mapper.ErpPurchasePlanAuditLogMapper;
|
||||
import com.klp.erp.mapper.ErpPurchasePlanMapper;
|
||||
import com.klp.erp.listener.ErpDeliveryExcelListener;
|
||||
import com.klp.erp.service.IErpPurchasePlanService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 采购计划Service业务层处理
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-06-22
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
|
||||
|
||||
private static final String PLAN_STATUS_ONGOING = "0";
|
||||
private static final String PLAN_STATUS_ARCHIVED = "1";
|
||||
private static final String AUDIT_PENDING = "0";
|
||||
private static final String AUDIT_PASS = "1";
|
||||
private static final String AUDIT_REJECT = "2";
|
||||
private static final String AUDIT_DRAFT = "3";
|
||||
private static final String ITEM_NOT_ARRIVED = "0";
|
||||
private static final String ITEM_PARTIAL = "1";
|
||||
private static final String ITEM_ARRIVED = "2";
|
||||
|
||||
private final ErpPurchasePlanMapper baseMapper;
|
||||
private final ErpPurchasePlanItemMapper itemMapper;
|
||||
private final ErpPurchasePlanContractRelMapper relMapper;
|
||||
private final ErpPurchasePlanDeliveryMapper deliveryMapper;
|
||||
private final ErpPurchasePlanAuditLogMapper auditLogMapper;
|
||||
|
||||
@Override
|
||||
public ErpPurchasePlanVo queryById(Long planId) {
|
||||
ErpPurchasePlanVo vo = baseMapper.selectVoById(planId);
|
||||
if (vo == null) {
|
||||
return null;
|
||||
}
|
||||
// 明细
|
||||
vo.setItems(itemMapper.selectVoList(Wrappers.lambdaQuery(ErpPurchasePlanItem.class)
|
||||
.eq(ErpPurchasePlanItem::getPlanId, planId)));
|
||||
// 关联合同
|
||||
List<Long> orderIds = relMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanContractRel.class)
|
||||
.eq(ErpPurchasePlanContractRel::getPlanId, planId)).stream()
|
||||
.map(ErpPurchasePlanContractRel::getOrderId).collect(Collectors.toList());
|
||||
vo.setOrderIds(orderIds);
|
||||
if (!orderIds.isEmpty()) {
|
||||
vo.setContractCodes(baseMapper.selectOrderCodes(orderIds));
|
||||
}
|
||||
vo.setProgress(calcProgress(vo.getArrivedWeight(), vo.getPlanWeight()));
|
||||
// 审核历史(含每次驳回理由)
|
||||
vo.setAuditLogs(queryAuditLogs(planId));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ErpPurchasePlanItemVo> queryItemsByOrders(List<Long> orderIds) {
|
||||
if (orderIds == null || orderIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return baseMapper.selectItemsByOrderIds(orderIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<ErpPurchasePlanVo> queryPageList(ErpPurchasePlanBo bo, PageQuery pageQuery) {
|
||||
Page<ErpPurchasePlanVo> result = baseMapper.selectVoPage(pageQuery.build(), buildQueryWrapper(bo));
|
||||
result.getRecords().forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight())));
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ErpPurchasePlanVo> queryList(ErpPurchasePlanBo bo) {
|
||||
List<ErpPurchasePlanVo> list = baseMapper.selectVoList(buildQueryWrapper(bo));
|
||||
list.forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight())));
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<ErpContractOptionVo> queryContractPage(String keyword, PageQuery pageQuery) {
|
||||
Page<ErpContractOptionVo> page = baseMapper.selectContractPage(pageQuery.build(), keyword);
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ErpPurchasePlanVo> queryPlansByContract(Long orderId) {
|
||||
List<ErpPurchasePlanVo> list = baseMapper.selectPlansByContract(orderId);
|
||||
list.forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight())));
|
||||
return list;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ErpPurchasePlan> buildQueryWrapper(ErpPurchasePlanBo bo) {
|
||||
LambdaQueryWrapper<ErpPurchasePlan> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getPlanNo()), ErpPurchasePlan::getPlanNo, bo.getPlanNo());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getPlanStatus()), ErpPurchasePlan::getPlanStatus, bo.getPlanStatus());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getAuditStatus()), ErpPurchasePlan::getAuditStatus, bo.getAuditStatus());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getSupplier()), ErpPurchasePlan::getSupplier, bo.getSupplier());
|
||||
// 综合关键字:计划号 / 供货商 / 合同号(合同号经中间表预查出 planId 再 OR 进来)
|
||||
if (StringUtils.isNotBlank(bo.getKeyword())) {
|
||||
String kw = bo.getKeyword().trim();
|
||||
List<Long> planIdsByContract = baseMapper.selectPlanIdsByContractKeyword(kw);
|
||||
lqw.and(w -> {
|
||||
w.like(ErpPurchasePlan::getPlanNo, kw)
|
||||
.or().like(ErpPurchasePlan::getSupplier, kw);
|
||||
if (!planIdsByContract.isEmpty()) {
|
||||
w.or().in(ErpPurchasePlan::getPlanId, planIdsByContract);
|
||||
}
|
||||
});
|
||||
}
|
||||
lqw.orderByDesc(ErpPurchasePlan::getPlanId);
|
||||
return lqw;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean insertByBo(ErpPurchasePlanBo bo) {
|
||||
ErpPurchasePlan add = BeanUtil.toBean(bo, ErpPurchasePlan.class);
|
||||
if (StringUtils.isBlank(add.getPlanNo())) {
|
||||
add.setPlanNo(generatePlanNo());
|
||||
}
|
||||
add.setPlanStatus(PLAN_STATUS_ONGOING);
|
||||
add.setAuditStatus(AUDIT_DRAFT); // 新建为「待送审」,需手动送审后才进入审核池
|
||||
add.setArrivedWeight(BigDecimal.ZERO);
|
||||
add.setPlanWeight(sumItemWeight(bo.getItems()));
|
||||
if (baseMapper.insert(add) <= 0) {
|
||||
return false;
|
||||
}
|
||||
bo.setPlanId(add.getPlanId());
|
||||
saveItems(add.getPlanId(), bo.getItems());
|
||||
saveContractRels(add.getPlanId(), bo.getOrderIds());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateByBo(ErpPurchasePlanBo bo) {
|
||||
ErpPurchasePlan update = baseMapper.selectById(bo.getPlanId());
|
||||
if (update == null) {
|
||||
throw new ServiceException("采购计划不存在");
|
||||
}
|
||||
update.setSupplier(bo.getSupplier());
|
||||
update.setPurchaseDate(bo.getPurchaseDate());
|
||||
update.setRemark(bo.getRemark());
|
||||
if (StringUtils.isNotBlank(bo.getPlanNo())) {
|
||||
update.setPlanNo(bo.getPlanNo());
|
||||
}
|
||||
update.setPlanWeight(sumItemWeight(bo.getItems()));
|
||||
baseMapper.updateById(update);
|
||||
// 覆盖式重写明细与合同关联
|
||||
itemMapper.delete(Wrappers.lambdaQuery(ErpPurchasePlanItem.class)
|
||||
.eq(ErpPurchasePlanItem::getPlanId, bo.getPlanId()));
|
||||
saveItems(bo.getPlanId(), bo.getItems());
|
||||
relMapper.delete(Wrappers.lambdaQuery(ErpPurchasePlanContractRel.class)
|
||||
.eq(ErpPurchasePlanContractRel::getPlanId, bo.getPlanId()));
|
||||
saveContractRels(bo.getPlanId(), bo.getOrderIds());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveItems(Long planId, List<ErpPurchasePlanItemBo> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (ErpPurchasePlanItemBo itemBo : items) {
|
||||
ErpPurchasePlanItem item = BeanUtil.toBean(itemBo, ErpPurchasePlanItem.class);
|
||||
item.setItemId(null);
|
||||
item.setPlanId(planId);
|
||||
itemMapper.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveContractRels(Long planId, List<Long> orderIds) {
|
||||
if (orderIds == null || orderIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Long orderId : orderIds) {
|
||||
if (orderId == null) {
|
||||
continue;
|
||||
}
|
||||
ErpPurchasePlanContractRel rel = new ErpPurchasePlanContractRel();
|
||||
rel.setPlanId(planId);
|
||||
rel.setOrderId(orderId);
|
||||
relMapper.insert(rel);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal sumItemWeight(List<ErpPurchasePlanItemBo> items) {
|
||||
if (items == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return items.stream()
|
||||
.map(i -> i.getWeight() == null ? BigDecimal.ZERO : i.getWeight())
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
for (Long planId : ids) {
|
||||
itemMapper.delete(Wrappers.lambdaQuery(ErpPurchasePlanItem.class).eq(ErpPurchasePlanItem::getPlanId, planId));
|
||||
relMapper.delete(Wrappers.lambdaQuery(ErpPurchasePlanContractRel.class).eq(ErpPurchasePlanContractRel::getPlanId, planId));
|
||||
deliveryMapper.delete(Wrappers.lambdaQuery(ErpPurchasePlanDelivery.class).eq(ErpPurchasePlanDelivery::getPlanId, planId));
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean audit(ErpPurchasePlanAuditBo bo, String operator) {
|
||||
if (!AUDIT_PASS.equals(bo.getAuditStatus()) && !AUDIT_REJECT.equals(bo.getAuditStatus())) {
|
||||
throw new ServiceException("审核结果只能为通过或驳回");
|
||||
}
|
||||
ErpPurchasePlan plan = baseMapper.selectById(bo.getPlanId());
|
||||
if (plan == null) {
|
||||
throw new ServiceException("采购计划不存在");
|
||||
}
|
||||
Date now = new Date();
|
||||
plan.setAuditStatus(bo.getAuditStatus());
|
||||
plan.setAuditOpinion(bo.getAuditOpinion());
|
||||
plan.setAuditor(operator);
|
||||
plan.setAuditTime(now);
|
||||
boolean ok = baseMapper.updateById(plan) > 0;
|
||||
// 每次审核留痕(含驳回后重新审核)
|
||||
ErpPurchasePlanAuditLog log = new ErpPurchasePlanAuditLog();
|
||||
log.setPlanId(plan.getPlanId());
|
||||
log.setAuditStatus(bo.getAuditStatus());
|
||||
log.setAuditOpinion(bo.getAuditOpinion());
|
||||
log.setAuditor(operator);
|
||||
log.setAuditTime(now);
|
||||
log.setCreateTime(now);
|
||||
auditLogMapper.insert(log);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean submitForAudit(Long planId) {
|
||||
ErpPurchasePlan plan = baseMapper.selectById(planId);
|
||||
if (plan == null) {
|
||||
throw new ServiceException("采购计划不存在");
|
||||
}
|
||||
if (!AUDIT_DRAFT.equals(plan.getAuditStatus()) && !AUDIT_REJECT.equals(plan.getAuditStatus())) {
|
||||
throw new ServiceException("仅「待送审」或「已驳回」的计划可送审");
|
||||
}
|
||||
plan.setAuditStatus(AUDIT_PENDING);
|
||||
return baseMapper.updateById(plan) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ErpPurchasePlanAuditLogVo> queryAuditLogs(Long planId) {
|
||||
return auditLogMapper.selectVoList(Wrappers.lambdaQuery(ErpPurchasePlanAuditLog.class)
|
||||
.eq(ErpPurchasePlanAuditLog::getPlanId, planId)
|
||||
.orderByDesc(ErpPurchasePlanAuditLog::getAuditTime));
|
||||
}
|
||||
|
||||
/** 单卷重量阈值(吨):超过则判定整份文件以 kg 录入(无单卷热轧钢卷重达 200t) */
|
||||
private static final BigDecimal KG_THRESHOLD = BigDecimal.valueOf(200);
|
||||
private static final BigDecimal KG_TO_T = BigDecimal.valueOf(1000);
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> importDelivery(Long planId, InputStream inputStream, String operator) {
|
||||
ErpPurchasePlan plan = baseMapper.selectById(planId);
|
||||
if (plan == null) {
|
||||
throw new ServiceException("采购计划不存在");
|
||||
}
|
||||
if (!AUDIT_PASS.equals(plan.getAuditStatus())) {
|
||||
throw new ServiceException("仅审核通过的采购计划可导入到货");
|
||||
}
|
||||
// 1) 读取 + 列校验 + 数值异常收集(监听器内完成)
|
||||
ErpDeliveryExcelListener listener = new ErpDeliveryExcelListener();
|
||||
try {
|
||||
EasyExcel.read(inputStream, ErpPurchasePlanDeliveryImportVo.class, listener).sheet().doRead();
|
||||
} catch (ServiceException se) {
|
||||
throw se;
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("到货文件无法解析,请确认为标准 Excel(.xlsx/.xls) 且格式正确");
|
||||
}
|
||||
if (!listener.getErrors().isEmpty()) {
|
||||
throw new ServiceException("到货文件存在问题:<br/>" + String.join("<br/>", listener.getErrors()));
|
||||
}
|
||||
List<ErpPurchasePlanDeliveryImportVo> rows = listener.getList();
|
||||
// 过滤空行(无卷号且无重量)
|
||||
List<ErpPurchasePlanDeliveryImportVo> valid = rows.stream()
|
||||
.filter(r -> StringUtils.isNotBlank(r.getCoilNo()) || r.getCoilWeight() != null)
|
||||
.collect(Collectors.toList());
|
||||
if (valid.isEmpty()) {
|
||||
throw new ServiceException("未解析到有效到货数据,请检查文件内容或列名是否与模板一致");
|
||||
}
|
||||
// 2) kg→t 单位判定(文件级:单卷重量最大值超阈值视为 kg)
|
||||
BigDecimal maxCoil = valid.stream()
|
||||
.map(ErpPurchasePlanDeliveryImportVo::getCoilWeight)
|
||||
.filter(w -> w != null)
|
||||
.max(BigDecimal::compareTo)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
boolean kgConverted = maxCoil.compareTo(KG_THRESHOLD) > 0;
|
||||
|
||||
// 3) 合并单元格向下填充 + 单位换算 + 落库
|
||||
String lastTruckNo = null;
|
||||
BigDecimal lastTruckWeight = null;
|
||||
Integer lastPieceCount = null;
|
||||
int count = 0;
|
||||
for (ErpPurchasePlanDeliveryImportVo row : rows) {
|
||||
if (StringUtils.isNotBlank(row.getTruckNo())) {
|
||||
lastTruckNo = row.getTruckNo();
|
||||
lastTruckWeight = row.getTruckWeight();
|
||||
lastPieceCount = row.getPieceCount();
|
||||
}
|
||||
if (StringUtils.isBlank(row.getCoilNo()) && row.getCoilWeight() == null) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal truckWeight = row.getTruckWeight() != null ? row.getTruckWeight() : lastTruckWeight;
|
||||
ErpPurchasePlanDelivery d = new ErpPurchasePlanDelivery();
|
||||
d.setPlanId(planId);
|
||||
d.setArrivalDate(parseDate(row.getArrivalDate()));
|
||||
d.setGrade(row.getGrade());
|
||||
d.setSpec(row.getSpec());
|
||||
d.setCoilNo(row.getCoilNo());
|
||||
d.setCoilWeight(convertWeight(row.getCoilWeight(), kgConverted));
|
||||
d.setTruckNo(StringUtils.isNotBlank(row.getTruckNo()) ? row.getTruckNo() : lastTruckNo);
|
||||
d.setTruckWeight(convertWeight(truckWeight, kgConverted));
|
||||
d.setPieceCount(row.getPieceCount() != null ? row.getPieceCount() : lastPieceCount);
|
||||
d.setSalesCode(row.getSalesCode());
|
||||
d.setArrivalStation(row.getArrivalStation());
|
||||
deliveryMapper.insert(d);
|
||||
count++;
|
||||
}
|
||||
refreshProgress(planId);
|
||||
|
||||
StringBuilder msg = new StringBuilder("成功导入 " + count + " 条到货记录");
|
||||
if (kgConverted) {
|
||||
msg.append(";检测到重量疑似按 kg 录入(最大单卷 ")
|
||||
.append(maxCoil.stripTrailingZeros().toPlainString())
|
||||
.append("),已自动 ÷1000 换算为吨");
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>(4);
|
||||
result.put("count", count);
|
||||
result.put("kgConverted", kgConverted);
|
||||
result.put("message", msg.toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
/** kg→t 换算:kgConverted 为真时 ÷1000,保留3位 */
|
||||
private BigDecimal convertWeight(BigDecimal v, boolean kgConverted) {
|
||||
if (v == null) {
|
||||
return null;
|
||||
}
|
||||
return kgConverted ? v.divide(KG_TO_T, 3, RoundingMode.HALF_UP) : v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ErpPurchasePlanDeliveryVo> queryDeliveryList(Long planId) {
|
||||
return deliveryMapper.selectVoList(Wrappers.lambdaQuery(ErpPurchasePlanDelivery.class)
|
||||
.eq(ErpPurchasePlanDelivery::getPlanId, planId)
|
||||
.orderByAsc(ErpPurchasePlanDelivery::getTruckNo, ErpPurchasePlanDelivery::getDeliveryId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteDelivery(Long deliveryId) {
|
||||
ErpPurchasePlanDelivery d = deliveryMapper.selectById(deliveryId);
|
||||
if (d == null) {
|
||||
return false;
|
||||
}
|
||||
boolean ok = deliveryMapper.deleteById(deliveryId) > 0;
|
||||
refreshProgress(d.getPlanId());
|
||||
return ok;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void refreshProgress(Long planId) {
|
||||
ErpPurchasePlan plan = baseMapper.selectById(planId);
|
||||
if (plan == null) {
|
||||
return;
|
||||
}
|
||||
// 1. 计划总到货 = Σ单卷重量
|
||||
BigDecimal arrived = deliveryMapper.sumCoilWeightByPlan(planId);
|
||||
if (arrived == null) {
|
||||
arrived = BigDecimal.ZERO;
|
||||
}
|
||||
plan.setArrivedWeight(arrived);
|
||||
|
||||
// 2. 明细级回填:到货行按 牌号+规格(厚×宽) 聚合,再顺序分配到匹配明细
|
||||
List<ErpPurchasePlanItem> items = itemMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanItem.class)
|
||||
.eq(ErpPurchasePlanItem::getPlanId, planId));
|
||||
List<ErpPurchasePlanDelivery> deliveries = deliveryMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanDelivery.class)
|
||||
.eq(ErpPurchasePlanDelivery::getPlanId, planId));
|
||||
|
||||
Map<String, BigDecimal> arrivedByKey = new HashMap<>();
|
||||
for (ErpPurchasePlanDelivery d : deliveries) {
|
||||
String key = specKey(d.getGrade(), d.getSpec());
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal w = d.getCoilWeight() == null ? BigDecimal.ZERO : d.getCoilWeight();
|
||||
arrivedByKey.merge(key, w, BigDecimal::add);
|
||||
}
|
||||
|
||||
// 明细按 key 分组(保持原顺序),便于同规格多行顺序分配
|
||||
Map<String, List<ErpPurchasePlanItem>> itemsByKey = new LinkedHashMap<>();
|
||||
for (ErpPurchasePlanItem it : items) {
|
||||
it.setArrivedWeight(BigDecimal.ZERO);
|
||||
it.setItemStatus(ITEM_NOT_ARRIVED);
|
||||
String key = itemSpecKey(it.getGrade(), it.getThickness(), it.getWidth());
|
||||
if (key != null) {
|
||||
itemsByKey.computeIfAbsent(key, k -> new ArrayList<>()).add(it);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, List<ErpPurchasePlanItem>> e : itemsByKey.entrySet()) {
|
||||
BigDecimal remaining = arrivedByKey.getOrDefault(e.getKey(), BigDecimal.ZERO);
|
||||
List<ErpPurchasePlanItem> group = e.getValue();
|
||||
for (int i = 0; i < group.size(); i++) {
|
||||
ErpPurchasePlanItem it = group.get(i);
|
||||
BigDecimal planned = it.getWeight() == null ? BigDecimal.ZERO : it.getWeight();
|
||||
// 末项吃掉剩余(含富余),其余按计划量封顶
|
||||
BigDecimal give = (i == group.size() - 1) ? remaining : remaining.min(planned);
|
||||
if (give.compareTo(BigDecimal.ZERO) < 0) {
|
||||
give = BigDecimal.ZERO;
|
||||
}
|
||||
it.setArrivedWeight(give);
|
||||
remaining = remaining.subtract(give);
|
||||
if (remaining.compareTo(BigDecimal.ZERO) < 0) {
|
||||
remaining = BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 明细状态 + 是否全部到货
|
||||
boolean allArrived = !items.isEmpty();
|
||||
for (ErpPurchasePlanItem it : items) {
|
||||
BigDecimal planned = it.getWeight() == null ? BigDecimal.ZERO : it.getWeight();
|
||||
BigDecimal aw = it.getArrivedWeight() == null ? BigDecimal.ZERO : it.getArrivedWeight();
|
||||
if (aw.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
it.setItemStatus(ITEM_NOT_ARRIVED);
|
||||
} else if (planned.compareTo(BigDecimal.ZERO) > 0 && aw.compareTo(planned) >= 0) {
|
||||
it.setItemStatus(ITEM_ARRIVED);
|
||||
} else {
|
||||
it.setItemStatus(ITEM_PARTIAL);
|
||||
}
|
||||
if (!ITEM_ARRIVED.equals(it.getItemStatus())) {
|
||||
allArrived = false;
|
||||
}
|
||||
itemMapper.updateById(it);
|
||||
}
|
||||
|
||||
// 4. 计划状态:所有明细到货 或 总量达标 → 自动归档
|
||||
BigDecimal planWeight = plan.getPlanWeight() == null ? BigDecimal.ZERO : plan.getPlanWeight();
|
||||
boolean weightDone = planWeight.compareTo(BigDecimal.ZERO) > 0 && arrived.compareTo(planWeight) >= 0;
|
||||
plan.setPlanStatus((allArrived || weightDone) ? PLAN_STATUS_ARCHIVED : PLAN_STATUS_ONGOING);
|
||||
baseMapper.updateById(plan);
|
||||
}
|
||||
|
||||
/** 到货行规格 key:牌号 + 厚×宽(规格形如 "3.00×1230") */
|
||||
private String specKey(String grade, String spec) {
|
||||
if (StringUtils.isBlank(spec)) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = spec.split("[×xX*]");
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
String t = normNum(parts[0]);
|
||||
String w = normNum(parts[1]);
|
||||
if (t == null || w == null) {
|
||||
return null;
|
||||
}
|
||||
return normGrade(grade) + "|" + t + "×" + w;
|
||||
}
|
||||
|
||||
/** 明细规格 key:牌号 + 厚×宽 */
|
||||
private String itemSpecKey(String grade, String thickness, String width) {
|
||||
String t = normNum(thickness);
|
||||
String w = normNum(width);
|
||||
if (t == null || w == null) {
|
||||
return null;
|
||||
}
|
||||
return normGrade(grade) + "|" + t + "×" + w;
|
||||
}
|
||||
|
||||
private String normGrade(String g) {
|
||||
return g == null ? "" : g.trim().toUpperCase();
|
||||
}
|
||||
|
||||
/** 数字归一:去尾零,无法解析(区间文本等)返回 null 不参与匹配 */
|
||||
private String normNum(String s) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(s.trim()).stripTrailingZeros().toPlainString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> statistics(ErpPurchasePlanBo bo) {
|
||||
Long total = baseMapper.selectCount(buildQueryWrapper(bo));
|
||||
LambdaQueryWrapper<ErpPurchasePlan> completedWrapper = buildQueryWrapper(bo)
|
||||
.eq(ErpPurchasePlan::getPlanStatus, PLAN_STATUS_ARCHIVED);
|
||||
Long completed = baseMapper.selectCount(completedWrapper);
|
||||
Map<String, Object> map = new HashMap<>(4);
|
||||
map.put("total", total == null ? 0 : total);
|
||||
map.put("completed", completed == null ? 0 : completed);
|
||||
return map;
|
||||
}
|
||||
|
||||
/** 进度百分比(0-100,保留2位) */
|
||||
private BigDecimal calcProgress(BigDecimal arrived, BigDecimal planWeight) {
|
||||
if (planWeight == null || planWeight.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal a = arrived == null ? BigDecimal.ZERO : arrived;
|
||||
BigDecimal pct = a.multiply(BigDecimal.valueOf(100)).divide(planWeight, 2, RoundingMode.HALF_UP);
|
||||
return pct.min(BigDecimal.valueOf(100));
|
||||
}
|
||||
|
||||
/** 自动生成计划号:CG + yyyyMMdd + 4位流水 */
|
||||
private String generatePlanNo() {
|
||||
String prefix = "CG" + DateUtil.format(new Date(), "yyyyMMdd");
|
||||
Long todayCount = baseMapper.selectCount(Wrappers.lambdaQuery(ErpPurchasePlan.class)
|
||||
.likeRight(ErpPurchasePlan::getPlanNo, prefix));
|
||||
long seq = (todayCount == null ? 0L : todayCount) + 1L;
|
||||
return prefix + String.format("%04d", seq);
|
||||
}
|
||||
|
||||
private Date parseDate(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return DateUtil.parse(text.trim());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user