diff --git a/docs/deleted-menu-backup.sql b/docs/deleted-menu-backup.sql new file mode 100644 index 000000000..a815dff12 --- /dev/null +++ b/docs/deleted-menu-backup.sql @@ -0,0 +1,20 @@ +-- 删除「采购需求」「采购看板」菜单前的备份 生成于脚本 库:klp-oa +-- 回滚:执行下面的 INSERT 即可恢复菜单与角色授权 + +INSERT INTO sys_menu (menu_id,menu_name,parent_id,order_num,path,component,query_param,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (1990706561827045378,'采购需求',1954721010120728578,0,'requirement','erp/requirement/index',NULL,1,0,'C','0','0',NULL,'nested','admin','2025-11-18 17:00:05','admin','2026-02-01 22:39:54',''); +INSERT INTO sys_menu (menu_id,menu_name,parent_id,order_num,path,component,query_param,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (1990706888819179521,'采购看板',1954721010120728578,6,'dashboard','erp/dashboard/index',NULL,1,0,'C','0','0',NULL,'dashboard','admin','2025-11-18 17:01:23','admin','2026-05-08 19:06:02',''); +INSERT INTO sys_role_menu (role_id,menu_id) VALUES (1993591990145687554,1990706561827045378); +INSERT INTO sys_role_menu (role_id,menu_id) VALUES (1993591990145687554,1990706888819179521); + +-- 第二批删除:供应商库/采购订单/收货记录/退货管理/采购汇总 +INSERT INTO sys_menu (menu_id,menu_name,parent_id,order_num,path,component,query_param,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (1955113252714999810,'供应商库',1954721010120728578,1,'supplier','erp/supplier/index',NULL,1,0,'C','0','0',NULL,'people','admin','2025-08-12 11:44:58','admin','2026-02-01 22:40:28',''); +INSERT INTO sys_menu (menu_id,menu_name,parent_id,order_num,path,component,query_param,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (1990705902968995841,'采购订单',1954721010120728578,1,'order','erp/order/index',NULL,1,0,'C','0','0',NULL,'edit','admin','2025-11-18 16:57:27','admin','2025-11-18 17:03:49',''); +INSERT INTO sys_menu (menu_id,menu_name,parent_id,order_num,path,component,query_param,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (1990706263360372737,'收货记录',1954721010120728578,1,'receipt','erp/receipt/index',NULL,1,0,'C','0','1',NULL,'log','admin','2025-11-18 16:58:53','admin','2026-05-08 19:06:09',''); +INSERT INTO sys_menu (menu_id,menu_name,parent_id,order_num,path,component,query_param,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (1990706419627556866,'采购汇总',1954721010120728578,13,'report','erp/report/index',NULL,1,0,'C','0','1',NULL,'druid','admin','2025-11-18 16:59:31','admin','2026-05-08 19:05:51',''); +INSERT INTO sys_menu (menu_id,menu_name,parent_id,order_num,path,component,query_param,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (1990706698213228545,'退货管理',1954721010120728578,5,'return','erp/return/index',NULL,1,0,'C','0','1',NULL,'guide','admin','2025-11-18 17:00:37','admin','2026-05-08 19:06:06',''); +INSERT INTO sys_role_menu (role_id,menu_id) VALUES (1993591990145687554,1955113252714999810); +INSERT INTO sys_role_menu (role_id,menu_id) VALUES (1993591990145687554,1990705902968995841); +INSERT INTO sys_role_menu (role_id,menu_id) VALUES (1993591990145687554,1990706263360372737); +INSERT INTO sys_role_menu (role_id,menu_id) VALUES (1993591990145687554,1990706419627556866); +INSERT INTO sys_role_menu (role_id,menu_id) VALUES (1993591990145687554,1990706698213228545); +qin diff --git a/docs/purchase-plan-ddl.sql b/docs/purchase-plan-ddl.sql new file mode 100644 index 000000000..7e247894d --- /dev/null +++ b/docs/purchase-plan-ddl.sql @@ -0,0 +1,131 @@ +-- 采购计划 (erp_purchase_plan) 相关 DDL +-- 在主库 jdbc:mysql://140.143.206.120:13306/klp-oa-test 上执行 + +-- 1. 采购计划头 +CREATE TABLE IF NOT EXISTS `erp_purchase_plan` ( + `plan_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '计划ID', + `plan_no` VARCHAR(64) NOT NULL COMMENT '采购计划号', + `plan_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '计划状态: 0-进行中 1-已完成归档', + `audit_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '审核状态: 0-待审核 1-通过 2-驳回', + `audit_opinion` VARCHAR(512) DEFAULT NULL COMMENT '申请/审核意见', + `auditor` VARCHAR(64) DEFAULT NULL COMMENT '审核人', + `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间', + `supplier` VARCHAR(128) DEFAULT NULL COMMENT '供货商', + `purchase_date` DATE DEFAULT NULL COMMENT '采购日期', + `plan_weight` DECIMAL(14,3) NOT NULL DEFAULT 0 COMMENT '计划总重量(T)', + `arrived_weight` DECIMAL(14,3) NOT NULL DEFAULT 0 COMMENT '已到货重量(T)', + `del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志: 0-存在 2-删除', + `create_by` VARCHAR(64) DEFAULT NULL, + `create_time` DATETIME DEFAULT NULL, + `update_by` VARCHAR(64) DEFAULT NULL, + `update_time` DATETIME DEFAULT NULL, + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`plan_id`), + UNIQUE KEY `uk_plan_no` (`plan_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购计划头'; + +-- 2. 采购计划明细(多规格行,字段对齐 crm_order_item) +CREATE TABLE IF NOT EXISTS `erp_purchase_plan_item` ( + `item_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '明细ID', + `plan_id` BIGINT NOT NULL COMMENT '关联计划ID', + `product_type` VARCHAR(64) DEFAULT NULL COMMENT '产品(如热轧卷板)', + `material` VARCHAR(64) DEFAULT NULL COMMENT '材质', + `grade` VARCHAR(64) DEFAULT NULL COMMENT '牌号', + `coil_no` VARCHAR(64) DEFAULT NULL COMMENT '卷号', + `width` VARCHAR(64) DEFAULT NULL COMMENT '宽度(mm,可为区间文本,对齐 crm_order_item)', + `thickness` VARCHAR(64) DEFAULT NULL COMMENT '厚度(mm,可为区间文本)', + `width_tolerance` VARCHAR(64) DEFAULT '0' COMMENT '宽度公差(自由文本)', + `thickness_tolerance` VARCHAR(64) DEFAULT '0' COMMENT '厚度公差(自由文本)', + `weight` DECIMAL(14,3) DEFAULT NULL COMMENT '重量(T)', + `quantity` INT DEFAULT NULL COMMENT '数量(件/卷数,来自合同 product_num)', + `arrived_weight` DECIMAL(14,3) NOT NULL DEFAULT 0 COMMENT '已到货重量(T),由到货Excel按牌号+规格累加', + `item_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '到货状态: 0-未到货 1-部分到货 2-已到货', + `supplier` VARCHAR(128) DEFAULT NULL COMMENT '供货商', + `del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志', + `create_by` VARCHAR(64) DEFAULT NULL, + `create_time` DATETIME DEFAULT NULL, + `update_by` VARCHAR(64) DEFAULT NULL, + `update_time` DATETIME DEFAULT NULL, + `remark` VARCHAR(512) DEFAULT NULL, + PRIMARY KEY (`item_id`), + KEY `idx_plan_id` (`plan_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购计划明细'; + +-- 3. 计划↔销售合同(crm_order) 中间表(多对多,挂合同头) +CREATE TABLE IF NOT EXISTS `erp_purchase_plan_contract_rel` ( + `rel_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '关系ID', + `plan_id` BIGINT NOT NULL COMMENT '采购计划ID', + `order_id` BIGINT NOT NULL COMMENT '销售合同ID(crm_order.order_id)', + `del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志', + `create_by` VARCHAR(64) DEFAULT NULL, + `create_time` DATETIME DEFAULT NULL, + `update_by` VARCHAR(64) DEFAULT NULL, + `update_time` DATETIME DEFAULT NULL, + `remark` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`rel_id`), + KEY `idx_plan_id` (`plan_id`), + KEY `idx_order_id` (`order_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购计划-销售合同关联表'; + +-- 4. 到货明细(对应上传的到货 Excel,一行一卷,独立于 WMS) +CREATE TABLE IF NOT EXISTS `erp_purchase_plan_delivery` ( + `delivery_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '到货明细ID', + `plan_id` BIGINT NOT NULL COMMENT '关联计划ID', + `arrival_date` DATE DEFAULT NULL COMMENT '日期', + `grade` VARCHAR(64) DEFAULT NULL COMMENT '牌号', + `spec` VARCHAR(64) DEFAULT NULL COMMENT '规格(厚×宽)', + `coil_no` VARCHAR(64) DEFAULT NULL COMMENT '卷号', + `coil_weight` DECIMAL(14,3) DEFAULT NULL COMMENT '单卷重量(T)', + `truck_no` VARCHAR(64) DEFAULT NULL COMMENT '车号', + `truck_weight` DECIMAL(14,3) DEFAULT NULL COMMENT '整车数量(T)', + `piece_count` INT DEFAULT NULL COMMENT '件数', + `sales_code` VARCHAR(64) DEFAULT NULL COMMENT '销售代码', + `arrival_station` VARCHAR(64) DEFAULT NULL COMMENT '钢厂到站', + `del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志', + `create_by` VARCHAR(64) DEFAULT NULL, + `create_time` DATETIME DEFAULT NULL, + `update_by` VARCHAR(64) DEFAULT NULL, + `update_time` DATETIME DEFAULT NULL, + `remark` VARCHAR(512) DEFAULT NULL, + PRIMARY KEY (`delivery_id`), + KEY `idx_plan_id` (`plan_id`), + KEY `idx_coil_no` (`coil_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购计划到货明细'; + + +-- ============ 菜单 ============ +-- 采购目录挂在「生产辅助」(path=helper) 下;父 id 按 path 动态解析,兼容不同环境的不同 menu_id。 +-- 整段可重复执行(叶子菜单用 REPLACE,目录用 存在判断 + 归位 UPDATE)。 + +-- 1) 解析「生产辅助」目录 id +SET @helper_id = (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE path = 'helper' AND parent_id = 0 LIMIT 1) t); + +-- 2) 确保「采购」目录存在(不存在则在「生产辅助」下创建;用派生表规避同表 INSERT...SELECT 限制) +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2100000000000000000, '采购', @helper_id, 50, 'purchase', '', '', 1, 0, 'M', '0', '0', '', 'shopping', 'admin', sysdate(), '采购目录' +FROM dual +WHERE NOT EXISTS (SELECT 1 FROM (SELECT menu_id FROM sys_menu WHERE path = 'purchase' AND menu_type = 'M') x); + +-- 3) 无论新建还是已存在,确保「采购」目录归位到「生产辅助」下,并设为显示(visible=0,否则其下所有菜单都不会出现在侧边栏) +UPDATE sys_menu SET parent_id = @helper_id, visible = '0', status = '0' WHERE path = 'purchase' AND menu_type = 'M'; + +-- 4) 解析「采购」目录 id(供下方叶子菜单使用) +SET @purchase_id = (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE path = 'purchase' AND menu_type = 'M' LIMIT 1) t); + +-- 5) 采购计划 + 按钮(REPLACE 可重复执行) +REPLACE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES +(2100000000000000001, '采购计划', @purchase_id, 2, 'purchasePlan', 'erp/purchasePlan/index', '', 1, 0, 'C', '0', '0', 'erp:purchasePlan:list', 'form', 'admin', sysdate(), '采购计划菜单'), +(2100000000000000002, '采购计划查询', 2100000000000000001, 1, '', '', '', 1, 0, 'F', '0', '0', 'erp:purchasePlan:query', '#', 'admin', sysdate(), ''), +(2100000000000000003, '采购计划新增', 2100000000000000001, 2, '', '', '', 1, 0, 'F', '0', '0', 'erp:purchasePlan:add', '#', 'admin', sysdate(), ''), +(2100000000000000004, '采购计划修改', 2100000000000000001, 3, '', '', '', 1, 0, 'F', '0', '0', 'erp:purchasePlan:edit', '#', 'admin', sysdate(), ''), +(2100000000000000005, '采购计划删除', 2100000000000000001, 4, '', '', '', 1, 0, 'F', '0', '0', 'erp:purchasePlan:remove', '#', 'admin', sysdate(), ''), +(2100000000000000006, '到货导入', 2100000000000000001, 5, '', '', '', 1, 0, 'F', '0', '0', 'erp:purchasePlan:import', '#', 'admin', sysdate(), ''); + +-- 6) 采购审核 + 按钮(REPLACE 可重复执行) +REPLACE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES +(2100000000000000010, '采购审核', @purchase_id, 3, 'purchaseAudit', 'erp/purchaseAudit/index', '', 1, 0, 'C', '0', '0', 'erp:purchasePlan:auditList', 'validCode', 'admin', sysdate(), '采购审核菜单'), +(2100000000000000011, '审核操作', 2100000000000000010, 1, '', '', '', 1, 0, 'F', '0', '0', 'erp:purchasePlan:audit', '#', 'admin', sysdate(), ''); + +-- 7) 采购进度(计划级到货进度总览) +REPLACE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES +(2100000000000000020, '采购进度', @purchase_id, 4, 'purchaseProgress', 'erp/purchaseProgress/index', '', 1, 0, 'C', '0', '0', 'erp:purchasePlan:list', 'data-line', 'admin', sysdate(), '采购进度总览'); diff --git a/klp-erp/src/main/java/com/klp/erp/controller/ErpPurchasePlanController.java b/klp-erp/src/main/java/com/klp/erp/controller/ErpPurchasePlanController.java new file mode 100644 index 000000000..65c37af9e --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/controller/ErpPurchasePlanController.java @@ -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 list(ErpPurchasePlanBo bo, PageQuery pageQuery) { + return iErpPurchasePlanService.queryPageList(bo, pageQuery); + } + + /** 总体到货进度统计(已完成 N / 共 M) */ + @GetMapping("/statistics") + public R> statistics(ErpPurchasePlanBo bo) { + return R.ok(iErpPurchasePlanService.statistics(bo)); + } + + /** 导出采购计划列表 */ + @Log(title = "采购计划", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(ErpPurchasePlanBo bo, HttpServletResponse response) { + List list = iErpPurchasePlanService.queryList(bo); + ExcelUtil.exportExcel(list, "采购计划", ErpPurchasePlanVo.class, response); + } + + /** 获取采购计划详细信息 */ + @GetMapping("/{planId}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long planId) { + return R.ok(iErpPurchasePlanService.queryById(planId)); + } + + /** 按销售合同取明细(选合同自动带出明细:1/2/3合同 -> 1/2/3/4明细) */ + @GetMapping("/itemsByOrders") + public R> itemsByOrders(@RequestParam("orderIds") List orderIds) { + return R.ok(iErpPurchasePlanService.queryItemsByOrders(orderIds)); + } + + /** 合同列表(左侧):crm_order + 每个合同已有的采购计划数 */ + @GetMapping("/contracts") + public TableDataInfo contracts(@RequestParam(value = "keyword", required = false) String keyword, + PageQuery pageQuery) { + return iErpPurchasePlanService.queryContractPage(keyword, pageQuery); + } + + /** 某合同下的所有采购计划 */ + @GetMapping("/byContract/{orderId}") + public R> byContract(@PathVariable Long orderId) { + return R.ok(iErpPurchasePlanService.queryPlansByContract(orderId)); + } + + /** 新增采购计划 */ + @Log(title = "采购计划", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody ErpPurchasePlanBo bo) { + return toAjax(iErpPurchasePlanService.insertByBo(bo)); + } + + /** 修改采购计划 */ + @Log(title = "采购计划", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody ErpPurchasePlanBo bo) { + return toAjax(iErpPurchasePlanService.updateByBo(bo)); + } + + /** 审核(通过/驳回 + 申请意见) */ + @Log(title = "采购计划", businessType = BusinessType.UPDATE) + @PutMapping("/audit") + public R audit(@Validated @RequestBody ErpPurchasePlanAuditBo bo) { + return toAjax(iErpPurchasePlanService.audit(bo, getUsername())); + } + + /** 送审 / 重新送审 */ + @Log(title = "采购计划", businessType = BusinessType.UPDATE) + @PutMapping("/submit/{planId}") + public R submit(@NotNull(message = "主键不能为空") @PathVariable Long planId) { + return toAjax(iErpPurchasePlanService.submitForAudit(planId)); + } + + /** 删除采购计划 */ + @Log(title = "采购计划", businessType = BusinessType.DELETE) + @DeleteMapping("/{planIds}") + public R 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> importDelivery(@PathVariable Long planId, + @RequestPart("file") MultipartFile file) throws Exception { + Map result = iErpPurchasePlanService.importDelivery(planId, file.getInputStream(), getUsername()); + return R.ok(String.valueOf(result.get("message")), result); + } + + /** 查询某计划的到货明细 */ + @GetMapping("/{planId}/delivery") + public R> deliveryList(@PathVariable Long planId) { + return R.ok(iErpPurchasePlanService.queryDeliveryList(planId)); + } + + /** 删除到货明细 */ + @Log(title = "采购计划-到货", businessType = BusinessType.DELETE) + @DeleteMapping("/delivery/{deliveryId}") + public R deleteDelivery(@PathVariable Long deliveryId) { + return toAjax(iErpPurchasePlanService.deleteDelivery(deliveryId)); + } +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlan.java b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlan.java new file mode 100644 index 000000000..21119fb5a --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlan.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanAuditLog.java b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanAuditLog.java new file mode 100644 index 000000000..781afb113 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanAuditLog.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanContractRel.java b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanContractRel.java new file mode 100644 index 000000000..7060d3c26 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanContractRel.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanDelivery.java b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanDelivery.java new file mode 100644 index 000000000..27b65be77 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanDelivery.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanItem.java b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanItem.java new file mode 100644 index 000000000..bcbf950cc --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/ErpPurchasePlanItem.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanAuditBo.java b/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanAuditBo.java new file mode 100644 index 000000000..b61071d4d --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanAuditBo.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanBo.java b/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanBo.java new file mode 100644 index 000000000..0edb6c2bc --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanBo.java @@ -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 items; + + /** 关联的销售合同ID列表(crm_order.order_id) */ + private List orderIds; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanItemBo.java b/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanItemBo.java new file mode 100644 index 000000000..22715a09b --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/bo/ErpPurchasePlanItemBo.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpContractOptionVo.java b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpContractOptionVo.java new file mode 100644 index 000000000..1e9f0eaad --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpContractOptionVo.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanAuditLogVo.java b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanAuditLogVo.java new file mode 100644 index 000000000..3b0478b02 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanAuditLogVo.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanDeliveryImportVo.java b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanDeliveryImportVo.java new file mode 100644 index 000000000..ad89b660d --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanDeliveryImportVo.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanDeliveryVo.java b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanDeliveryVo.java new file mode 100644 index 000000000..2d2cb3cd9 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanDeliveryVo.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanItemVo.java b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanItemVo.java new file mode 100644 index 000000000..c692cb030 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanItemVo.java @@ -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; +} diff --git a/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanVo.java b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanVo.java new file mode 100644 index 000000000..eefb6a076 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/domain/vo/ErpPurchasePlanVo.java @@ -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 items; + + /** 关联销售合同ID */ + private List orderIds; + + /** 关联销售合同编号(展示用) */ + private List contractCodes; + + /** 审核历史(每次审核/驳回留痕) */ + private List auditLogs; +} diff --git a/klp-erp/src/main/java/com/klp/erp/listener/ErpDeliveryExcelListener.java b/klp-erp/src/main/java/com/klp/erp/listener/ErpDeliveryExcelListener.java new file mode 100644 index 000000000..f7c9121d4 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/listener/ErpDeliveryExcelListener.java @@ -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 导入监听器 + *

+ * 负责:表头/列名校验、逐行数值转换异常收集(不中断),供 Service 统一反馈。 + * + * @author klp + * @date 2026-06-22 + */ +public class ErpDeliveryExcelListener extends AnalysisEventListener { + + /** 必需列(与到货模板表头一致) */ + private static final List REQUIRED_HEADERS = Arrays.asList("日期", "牌号", "规格", "卷号", "单卷重量"); + + private final List list = new ArrayList<>(); + private final List errors = new ArrayList<>(); + private Map headMap; + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + this.headMap = headMap; + List headers = headMap.values().stream() + .filter(h -> h != null) + .map(String::trim) + .collect(Collectors.toList()); + List 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 getList() { + return list; + } + + public List getErrors() { + return errors; + } +} diff --git a/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanAuditLogMapper.java b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanAuditLogMapper.java new file mode 100644 index 000000000..34ade91f0 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanAuditLogMapper.java @@ -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 { +} diff --git a/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanContractRelMapper.java b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanContractRelMapper.java new file mode 100644 index 000000000..99885c2ba --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanContractRelMapper.java @@ -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 { + +} diff --git a/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanDeliveryMapper.java b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanDeliveryMapper.java new file mode 100644 index 000000000..88886ee6c --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanDeliveryMapper.java @@ -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 { + + /** + * 汇总某计划下的已到货重量(Σ单卷重量) + */ + BigDecimal sumCoilWeightByPlan(@Param("planId") Long planId); +} diff --git a/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanItemMapper.java b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanItemMapper.java new file mode 100644 index 000000000..db4b2b402 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanItemMapper.java @@ -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 { + +} diff --git a/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanMapper.java b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanMapper.java new file mode 100644 index 000000000..71c18b8e9 --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/mapper/ErpPurchasePlanMapper.java @@ -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 { + + /** + * 根据销售合同ID查询合同编号(crm_order,同库跨表) + */ + List selectOrderCodes(@Param("ids") List ids); + + /** + * 按销售合同ID批量取明细,映射为采购计划明细(来自 crm_order_item)。 + * 用于「选合同自动带出明细」:1/2/3合同 -> 1/2/3/4明细。 + */ + List selectItemsByOrderIds(@Param("ids") List ids); + + /** + * 合同列表分页:crm_order + 该合同已挂接的采购计划数。 + */ + Page selectContractPage(IPage page, @Param("kw") String kw); + + /** + * 某合同下的所有采购计划(经中间表挂接)。 + */ + List selectPlansByContract(@Param("orderId") Long orderId); + + /** + * 按合同关键字(订单编号/合同号/合同名称)查出关联的采购计划ID,用于综合搜索。 + */ + List selectPlanIdsByContractKeyword(@Param("kw") String kw); +} diff --git a/klp-erp/src/main/java/com/klp/erp/service/IErpPurchasePlanService.java b/klp-erp/src/main/java/com/klp/erp/service/IErpPurchasePlanService.java new file mode 100644 index 000000000..9b3a586ab --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/service/IErpPurchasePlanService.java @@ -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 queryItemsByOrders(List orderIds); + + /** 合同列表分页(含每个合同已有的采购计划数),用于采购计划页左侧 */ + TableDataInfo queryContractPage(String keyword, PageQuery pageQuery); + + /** 某合同下的所有采购计划 */ + List queryPlansByContract(Long orderId); + + /** 分页查询采购计划 */ + TableDataInfo queryPageList(ErpPurchasePlanBo bo, PageQuery pageQuery); + + /** 查询采购计划列表 */ + List queryList(ErpPurchasePlanBo bo); + + /** 新增采购计划(含明细、合同挂接) */ + Boolean insertByBo(ErpPurchasePlanBo bo); + + /** 修改采购计划(含明细、合同挂接) */ + Boolean updateByBo(ErpPurchasePlanBo bo); + + /** 校验并批量删除采购计划 */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** 审核(通过/驳回 + 申请意见),每次审核留痕 */ + Boolean audit(ErpPurchasePlanAuditBo bo, String operator); + + /** 送审 / 重新送审:待送审(3) 或 已驳回(2) → 待审核(0),之后才进入审核页 */ + Boolean submitForAudit(Long planId); + + /** 某计划的审核历史 */ + List queryAuditLogs(Long planId); + + /** + * 导入到货 Excel:校验列/数值、kg→t 单位纠正,写入并刷新进度归档。 + * 返回 {count: 入库条数, message: 回执文案, kgConverted: 是否做了单位换算} + */ + Map importDelivery(Long planId, InputStream inputStream, String operator); + + /** 查询某计划的到货明细 */ + List queryDeliveryList(Long planId); + + /** 删除到货明细,并刷新进度 */ + Boolean deleteDelivery(Long deliveryId); + + /** 刷新到货进度:arrivedWeight = Σ单卷重量;满 100% 自动归档 */ + void refreshProgress(Long planId); + + /** 总体到货进度统计(已完成 N / 共 M) */ + Map statistics(ErpPurchasePlanBo bo); +} diff --git a/klp-erp/src/main/java/com/klp/erp/service/impl/ErpPurchasePlanServiceImpl.java b/klp-erp/src/main/java/com/klp/erp/service/impl/ErpPurchasePlanServiceImpl.java new file mode 100644 index 000000000..3e7d34f1c --- /dev/null +++ b/klp-erp/src/main/java/com/klp/erp/service/impl/ErpPurchasePlanServiceImpl.java @@ -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 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 queryItemsByOrders(List orderIds) { + if (orderIds == null || orderIds.isEmpty()) { + return new ArrayList<>(); + } + return baseMapper.selectItemsByOrderIds(orderIds); + } + + @Override + public TableDataInfo queryPageList(ErpPurchasePlanBo bo, PageQuery pageQuery) { + Page 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 queryList(ErpPurchasePlanBo bo) { + List list = baseMapper.selectVoList(buildQueryWrapper(bo)); + list.forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight()))); + return list; + } + + @Override + public TableDataInfo queryContractPage(String keyword, PageQuery pageQuery) { + Page page = baseMapper.selectContractPage(pageQuery.build(), keyword); + return TableDataInfo.build(page); + } + + @Override + public List queryPlansByContract(Long orderId) { + List list = baseMapper.selectPlansByContract(orderId); + list.forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight()))); + return list; + } + + private LambdaQueryWrapper buildQueryWrapper(ErpPurchasePlanBo bo) { + LambdaQueryWrapper 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 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 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 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 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 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 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 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("到货文件存在问题:
" + String.join("
", listener.getErrors())); + } + List rows = listener.getList(); + // 过滤空行(无卷号且无重量) + List 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 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 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 items = itemMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanItem.class) + .eq(ErpPurchasePlanItem::getPlanId, planId)); + List deliveries = deliveryMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanDelivery.class) + .eq(ErpPurchasePlanDelivery::getPlanId, planId)); + + Map 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> 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> e : itemsByKey.entrySet()) { + BigDecimal remaining = arrivedByKey.getOrDefault(e.getKey(), BigDecimal.ZERO); + List 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 statistics(ErpPurchasePlanBo bo) { + Long total = baseMapper.selectCount(buildQueryWrapper(bo)); + LambdaQueryWrapper completedWrapper = buildQueryWrapper(bo) + .eq(ErpPurchasePlan::getPlanStatus, PLAN_STATUS_ARCHIVED); + Long completed = baseMapper.selectCount(completedWrapper); + Map 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; + } + } +} diff --git a/klp-erp/src/main/resources/mapper/erp/ErpPurchasePlanDeliveryMapper.xml b/klp-erp/src/main/resources/mapper/erp/ErpPurchasePlanDeliveryMapper.xml new file mode 100644 index 000000000..1a3eb20c7 --- /dev/null +++ b/klp-erp/src/main/resources/mapper/erp/ErpPurchasePlanDeliveryMapper.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/klp-erp/src/main/resources/mapper/erp/ErpPurchasePlanMapper.xml b/klp-erp/src/main/resources/mapper/erp/ErpPurchasePlanMapper.xml new file mode 100644 index 000000000..42c805b26 --- /dev/null +++ b/klp-erp/src/main/resources/mapper/erp/ErpPurchasePlanMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + diff --git a/klp-ui/src/api/erp/purchasePlan.js b/klp-ui/src/api/erp/purchasePlan.js new file mode 100644 index 000000000..5854d2e1c --- /dev/null +++ b/klp-ui/src/api/erp/purchasePlan.js @@ -0,0 +1,124 @@ +import request from '@/utils/request' + +// 采购计划列表 +export function listPurchasePlan(query) { + return request({ + url: '/erp/purchasePlan/list', + method: 'get', + params: query + }) +} + +// 总体到货进度统计(已完成 N / 共 M) +export function purchasePlanStatistics(query) { + return request({ + url: '/erp/purchasePlan/statistics', + method: 'get', + params: query + }) +} + +// 采购计划详情 +export function getPurchasePlan(planId) { + return request({ + url: '/erp/purchasePlan/' + planId, + method: 'get' + }) +} + +// 按销售合同取明细(选合同自动带出明细) +export function getItemsByOrders(orderIds) { + return request({ + url: '/erp/purchasePlan/itemsByOrders', + method: 'get', + params: { orderIds: (orderIds || []).join(',') } + }) +} + +// 合同列表(左侧,含每个合同已有计划数) +export function listContracts(query) { + return request({ + url: '/erp/purchasePlan/contracts', + method: 'get', + params: query + }) +} + +// 某合同下的所有采购计划 +export function listPlansByContract(orderId) { + return request({ + url: '/erp/purchasePlan/byContract/' + orderId, + method: 'get' + }) +} + +// 新增采购计划 +export function addPurchasePlan(data) { + return request({ + url: '/erp/purchasePlan', + method: 'post', + data + }) +} + +// 修改采购计划 +export function updatePurchasePlan(data) { + return request({ + url: '/erp/purchasePlan', + method: 'put', + data + }) +} + +// 审核(通过/驳回 + 申请意见) +export function auditPurchasePlan(data) { + return request({ + url: '/erp/purchasePlan/audit', + method: 'put', + data + }) +} + +// 送审 / 重新送审 +export function submitPurchasePlan(planId) { + return request({ + url: '/erp/purchasePlan/submit/' + planId, + method: 'put' + }) +} + +// 删除采购计划 +export function delPurchasePlan(planIds) { + return request({ + url: '/erp/purchasePlan/' + planIds, + method: 'delete' + }) +} + +// 导入到货 Excel +export function importDelivery(planId, file) { + const formData = new FormData() + formData.append('file', file) + return request({ + url: `/erp/purchasePlan/${planId}/importDelivery`, + method: 'post', + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +// 某计划的到货明细 +export function listDelivery(planId) { + return request({ + url: `/erp/purchasePlan/${planId}/delivery`, + method: 'get' + }) +} + +// 删除到货明细 +export function delDelivery(deliveryId) { + return request({ + url: `/erp/purchasePlan/delivery/${deliveryId}`, + method: 'delete' + }) +} diff --git a/klp-ui/src/assets/styles/element-ui.scss b/klp-ui/src/assets/styles/element-ui.scss index b98be4b0d..deacf7892 100644 --- a/klp-ui/src/assets/styles/element-ui.scss +++ b/klp-ui/src/assets/styles/element-ui.scss @@ -209,6 +209,17 @@ body { } } +// 基础按钮:统一高度 + 垂直居中,确保与输入框/其它按钮在同一行对齐 +// (此前只有主色等 variant 走 mixin 设了高度,普通/朴素/append 按钮没设导致错位) +.el-button { + height: $--btn-height; + padding-top: 0; + padding-bottom: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + // 主按钮(品牌梯度) .el-button--primary { @include button-variant($--color-text-primary, darken($--color-primary, 10%)); @@ -615,11 +626,16 @@ body { margin-right: 4px; font-size: 12px; - // 标签样式 + // 标签样式:行高与控件(24px)一致,保证标签与输入框/选择器在同一水平线 .el-form-item__label { color: $--color-text-secondary; padding-right: $--spacing-base; font-size: 12px; + line-height: $--btn-height; + } + + .el-form-item__content { + line-height: $--btn-height; } // 搜索表单 inline 布局 @@ -641,6 +657,7 @@ body { // 输入框(统一高度 + 金属内阴影) .el-input { height: $--btn-height; + vertical-align: middle; // 与同行 el-button 对齐(按钮为 middle,避免基线错位) .el-input__inner { background: $--metal-gradient-light; @@ -782,9 +799,36 @@ body { height: 24px !important; } + // 多选:有标签时让输入框高度自适应,避免被固定 24px 裁切(标签/关闭×显示错位) + .el-select__tags + .el-input .el-input__inner { + height: auto !important; + min-height: 24px !important; + } + .el-select__tags { + max-width: calc(100% - 30px); + flex-wrap: wrap; + .el-tag { - max-width: 100px; + max-width: 100%; + display: inline-flex; + align-items: center; + box-sizing: border-box; + padding: 0 4px 0 6px; + + // 标签文字超长省略,不再把关闭按钮挤出 + .el-select__tags-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; + } + + .el-tag__close { + flex-shrink: 0; + margin-left: 3px; + transform: scale(0.9); + } } } diff --git a/klp-ui/src/views/erp/dashboard/index.vue b/klp-ui/src/views/erp/dashboard/index.vue deleted file mode 100644 index b2b4f4393..000000000 --- a/klp-ui/src/views/erp/dashboard/index.vue +++ /dev/null @@ -1,804 +0,0 @@ - - - - - \ No newline at end of file diff --git a/klp-ui/src/views/erp/order/index.vue b/klp-ui/src/views/erp/order/index.vue deleted file mode 100644 index 9e01fa0af..000000000 --- a/klp-ui/src/views/erp/order/index.vue +++ /dev/null @@ -1,490 +0,0 @@ - - - - - - diff --git a/klp-ui/src/views/erp/purchaseAudit/index.vue b/klp-ui/src/views/erp/purchaseAudit/index.vue new file mode 100644 index 000000000..fd9ddf0e2 --- /dev/null +++ b/klp-ui/src/views/erp/purchaseAudit/index.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/klp-ui/src/views/erp/purchasePlan/index.vue b/klp-ui/src/views/erp/purchasePlan/index.vue new file mode 100644 index 000000000..fb977b5ec --- /dev/null +++ b/klp-ui/src/views/erp/purchasePlan/index.vue @@ -0,0 +1,894 @@ + + + + + diff --git a/klp-ui/src/views/erp/receipt/index.vue b/klp-ui/src/views/erp/receipt/index.vue deleted file mode 100644 index 35fa4a08e..000000000 --- a/klp-ui/src/views/erp/receipt/index.vue +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - diff --git a/klp-ui/src/views/erp/report/index.vue b/klp-ui/src/views/erp/report/index.vue deleted file mode 100644 index de100e702..000000000 --- a/klp-ui/src/views/erp/report/index.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - diff --git a/klp-ui/src/views/erp/requirement/index.vue b/klp-ui/src/views/erp/requirement/index.vue deleted file mode 100644 index 067bae917..000000000 --- a/klp-ui/src/views/erp/requirement/index.vue +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - diff --git a/klp-ui/src/views/erp/return/index.vue b/klp-ui/src/views/erp/return/index.vue deleted file mode 100644 index f8b8d56dc..000000000 --- a/klp-ui/src/views/erp/return/index.vue +++ /dev/null @@ -1,393 +0,0 @@ - - - - - - diff --git a/klp-ui/src/views/erp/supplier/index.vue b/klp-ui/src/views/erp/supplier/index.vue index a246a2c24..0733a4e12 100644 --- a/klp-ui/src/views/erp/supplier/index.vue +++ b/klp-ui/src/views/erp/supplier/index.vue @@ -1,545 +1,232 @@ -