Merge remote-tracking branch 'origin/feat/erp-purchase-plan' into 0.8.X
This commit is contained in:
20
docs/deleted-menu-backup.sql
Normal file
20
docs/deleted-menu-backup.sql
Normal file
@@ -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
|
||||
131
docs/purchase-plan-ddl.sql
Normal file
131
docs/purchase-plan-ddl.sql
Normal file
@@ -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(), '采购进度总览');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.klp.erp.mapper.ErpPurchasePlanDeliveryMapper">
|
||||
|
||||
<select id="sumCoilWeightByPlan" resultType="java.math.BigDecimal">
|
||||
SELECT COALESCE(SUM(coil_weight), 0)
|
||||
FROM erp_purchase_plan_delivery
|
||||
WHERE del_flag = '0' AND plan_id = #{planId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.klp.erp.mapper.ErpPurchasePlanMapper">
|
||||
|
||||
<select id="selectOrderCodes" resultType="java.lang.String">
|
||||
SELECT order_code FROM crm_order
|
||||
WHERE del_flag = 0 AND order_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<!-- 选合同自动带出明细:crm_order_item -> 采购计划明细VO;公差空值给 '0' -->
|
||||
<select id="selectItemsByOrderIds" resultType="com.klp.erp.domain.vo.ErpPurchasePlanItemVo">
|
||||
SELECT
|
||||
product_type AS productType,
|
||||
material AS material,
|
||||
grade AS grade,
|
||||
width AS width,
|
||||
thickness AS thickness,
|
||||
COALESCE(NULLIF(TRIM(width_tolerance), ''), '0') AS widthTolerance,
|
||||
COALESCE(NULLIF(TRIM(thickness_tolerance), ''), '0') AS thicknessTolerance,
|
||||
weight AS weight,
|
||||
product_num AS quantity
|
||||
FROM crm_order_item
|
||||
WHERE del_flag = 0 AND order_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
ORDER BY order_id, item_id
|
||||
</select>
|
||||
|
||||
<!-- 合同列表 + 该合同已挂接的采购计划数 -->
|
||||
<select id="selectContractPage" resultType="com.klp.erp.domain.vo.ErpContractOptionVo">
|
||||
SELECT
|
||||
o.order_id AS orderId,
|
||||
o.order_code AS orderCode,
|
||||
o.contract_code AS contractCode,
|
||||
o.contract_name AS contractName,
|
||||
o.customer AS customer,
|
||||
o.supplier AS supplier,
|
||||
o.order_amount AS orderAmount,
|
||||
o.salesman AS salesman,
|
||||
(SELECT COUNT(*) FROM erp_purchase_plan_contract_rel r
|
||||
JOIN erp_purchase_plan p ON p.plan_id = r.plan_id AND p.del_flag = '0'
|
||||
WHERE r.del_flag = '0' AND r.order_id = o.order_id) AS planCount
|
||||
FROM crm_order o
|
||||
WHERE o.del_flag = 0
|
||||
<if test="kw != null and kw != ''">
|
||||
AND (o.order_code LIKE CONCAT('%', #{kw}, '%')
|
||||
OR o.contract_name LIKE CONCAT('%', #{kw}, '%')
|
||||
OR o.contract_code LIKE CONCAT('%', #{kw}, '%')
|
||||
OR o.customer LIKE CONCAT('%', #{kw}, '%'))
|
||||
</if>
|
||||
ORDER BY o.is_top DESC, o.order_id DESC
|
||||
</select>
|
||||
|
||||
<!-- 某合同下的所有采购计划 -->
|
||||
<select id="selectPlansByContract" resultType="com.klp.erp.domain.vo.ErpPurchasePlanVo">
|
||||
SELECT p.*
|
||||
FROM erp_purchase_plan p
|
||||
JOIN erp_purchase_plan_contract_rel r ON r.plan_id = p.plan_id AND r.del_flag = '0'
|
||||
WHERE p.del_flag = '0' AND r.order_id = #{orderId}
|
||||
ORDER BY p.plan_id DESC
|
||||
</select>
|
||||
|
||||
<!-- 按合同关键字查关联的采购计划ID(订单编号/合同号/合同名称) -->
|
||||
<select id="selectPlanIdsByContractKeyword" resultType="java.lang.Long">
|
||||
SELECT DISTINCT r.plan_id
|
||||
FROM erp_purchase_plan_contract_rel r
|
||||
JOIN crm_order o ON o.order_id = r.order_id AND o.del_flag = 0
|
||||
WHERE r.del_flag = '0'
|
||||
AND (o.order_code LIKE CONCAT('%', #{kw}, '%')
|
||||
OR o.contract_code LIKE CONCAT('%', #{kw}, '%')
|
||||
OR o.contract_name LIKE CONCAT('%', #{kw}, '%'))
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
124
klp-ui/src/api/erp/purchasePlan.js
Normal file
124
klp-ui/src/api/erp/purchasePlan.js
Normal file
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,804 +0,0 @@
|
||||
<template>
|
||||
<div class="erp-dashboard-page">
|
||||
<section class="stats-section">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon supplier-icon">
|
||||
<i class="el-icon-office-building"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ supplierCount }}</div>
|
||||
<div class="stat-label">供应商总数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon order-icon">
|
||||
<i class="el-icon-document"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ orderCount }}</div>
|
||||
<div class="stat-label">采购订单总数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon doing-icon">
|
||||
<i class="el-icon-loading"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ doingOrderCount }}</div>
|
||||
<div class="stat-label">执行中订单</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon complete-icon">
|
||||
<i class="el-icon-circle-check"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ completeOrderCount }}</div>
|
||||
<div class="stat-label">已完成订单</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="charts-section">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>订单状态分布</span>
|
||||
</div>
|
||||
<div ref="statusChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>供应商订单占比</span>
|
||||
</div>
|
||||
<div ref="supplierChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-section">
|
||||
<div class="chart-card full-width">
|
||||
<div class="chart-header">
|
||||
<span>订单趋势(按天)</span>
|
||||
</div>
|
||||
<div ref="trendChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-section">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>业务员订单分布</span>
|
||||
</div>
|
||||
<div ref="salesmanBarChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>部门订单分布</span>
|
||||
</div>
|
||||
<div ref="deptBarChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>按业务员订单占比(饼图)</span>
|
||||
</div>
|
||||
<div ref="salesmanPieChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>按部门订单占比(饼图)</span>
|
||||
</div>
|
||||
<div ref="deptPieChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-section">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>供应商信用等级分布</span>
|
||||
</div>
|
||||
<div ref="creditChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span>供应商类型分布</span>
|
||||
</div>
|
||||
<div ref="typeChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listSupplier, listPurchaseOrder } from '@/api/erp/purchase'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'ErpDashboard',
|
||||
data() {
|
||||
return {
|
||||
supplierCount: 0,
|
||||
orderCount: 0,
|
||||
doingOrderCount: 0,
|
||||
completeOrderCount: 0,
|
||||
supplierList: [],
|
||||
orderList: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.statusChart) this.statusChart.dispose()
|
||||
if (this.supplierChart) this.supplierChart.dispose()
|
||||
if (this.trendChart) this.trendChart.dispose()
|
||||
if (this.creditChart) this.creditChart.dispose()
|
||||
if (this.typeChart) this.typeChart.dispose()
|
||||
if (this.salesmanBarChart) this.salesmanBarChart.dispose()
|
||||
if (this.salesmanPieChart) this.salesmanPieChart.dispose()
|
||||
if (this.deptBarChart) this.deptBarChart.dispose()
|
||||
if (this.deptPieChart) this.deptPieChart.dispose()
|
||||
if (this.supplierBarChart) this.supplierBarChart.dispose()
|
||||
if (this.supplierPieChart) this.supplierPieChart.dispose()
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const [supplierRes, orderRes] = await Promise.all([
|
||||
listSupplier({ pageNum: 1, pageSize: 100000 }),
|
||||
listPurchaseOrder({ pageNum: 1, pageSize: 100000 })
|
||||
])
|
||||
this.supplierList = supplierRes.rows || []
|
||||
this.orderList = orderRes.rows || []
|
||||
this.supplierCount = this.supplierList.length
|
||||
this.orderCount = this.orderList.length
|
||||
this.doingOrderCount = this.orderList.filter(o => o.orderStatus === 1).length
|
||||
this.completeOrderCount = this.orderList.filter(o => o.orderStatus === 3).length
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.initCharts()
|
||||
})
|
||||
} catch (error) {
|
||||
this.$message.error('加载数据失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
initCharts() {
|
||||
this.initStatusChart()
|
||||
this.initSupplierChart()
|
||||
this.initTrendChart()
|
||||
this.initCreditChart()
|
||||
this.initTypeChart()
|
||||
this.initSalesmanCharts()
|
||||
this.initDeptCharts()
|
||||
this.initSupplierNameCharts()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
handleResize() {
|
||||
this.statusChart && this.statusChart.resize()
|
||||
this.supplierChart && this.supplierChart.resize()
|
||||
this.trendChart && this.trendChart.resize()
|
||||
this.creditChart && this.creditChart.resize()
|
||||
this.typeChart && this.typeChart.resize()
|
||||
this.salesmanBarChart && this.salesmanBarChart.resize()
|
||||
this.salesmanPieChart && this.salesmanPieChart.resize()
|
||||
this.deptBarChart && this.deptBarChart.resize()
|
||||
this.deptPieChart && this.deptPieChart.resize()
|
||||
this.supplierBarChart && this.supplierBarChart.resize()
|
||||
this.supplierPieChart && this.supplierPieChart.resize()
|
||||
},
|
||||
initStatusChart() {
|
||||
this.statusChart = echarts.init(this.$refs.statusChart)
|
||||
const statusMap = {
|
||||
0: '草稿',
|
||||
1: '执行中',
|
||||
2: '部分到货',
|
||||
3: '已完成',
|
||||
4: '已取消'
|
||||
}
|
||||
const statusData = {}
|
||||
this.orderList.forEach(order => {
|
||||
const status = order.orderStatus || 0
|
||||
statusData[status] = (statusData[status] || 0) + 1
|
||||
})
|
||||
const data = Object.keys(statusData).map(key => ({
|
||||
name: statusMap[key],
|
||||
value: statusData[key]
|
||||
}))
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
this.statusChart.setOption(option)
|
||||
},
|
||||
initSupplierChart() {
|
||||
this.supplierChart = echarts.init(this.$refs.supplierChart)
|
||||
const supplierOrderCount = {}
|
||||
this.orderList.forEach(order => {
|
||||
const supplierName = order.supplierName || '未知'
|
||||
supplierOrderCount[supplierName] = (supplierOrderCount[supplierName] || 0) + 1
|
||||
})
|
||||
let data = Object.keys(supplierOrderCount).map(name => ({
|
||||
name,
|
||||
value: supplierOrderCount[name]
|
||||
}))
|
||||
data = data.sort((a, b) => b.value - a.value).slice(0, 10)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '供应商订单',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
this.supplierChart.setOption(option)
|
||||
},
|
||||
initTrendChart() {
|
||||
this.trendChart = echarts.init(this.$refs.trendChart)
|
||||
const dayData = {}
|
||||
this.orderList.forEach(order => {
|
||||
if (order.orderDate) {
|
||||
const day = order.orderDate
|
||||
dayData[day] = (dayData[day] || 0) + 1
|
||||
}
|
||||
})
|
||||
const days = Object.keys(dayData).sort()
|
||||
const counts = days.map(d => dayData[d])
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['订单数量']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: days,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '订单数量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: counts,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(80, 141, 255, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(80, 141, 255, 0.1)' }
|
||||
])
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#508dff'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#508dff'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.trendChart.setOption(option)
|
||||
},
|
||||
initCreditChart() {
|
||||
this.creditChart = echarts.init(this.$refs.creditChart)
|
||||
const creditData = {}
|
||||
this.supplierList.forEach(supplier => {
|
||||
const credit = supplier.creditRating || '-'
|
||||
creditData[credit] = (creditData[credit] || 0) + 1
|
||||
})
|
||||
const data = Object.keys(creditData).map(key => ({
|
||||
name: key,
|
||||
value: creditData[key]
|
||||
}))
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Object.keys(creditData)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '供应商数量',
|
||||
type: 'bar',
|
||||
data: Object.values(creditData),
|
||||
itemStyle: {
|
||||
color: function (params) {
|
||||
const colorMap = {
|
||||
'A': '#67c23a',
|
||||
'B': '#e6a23c',
|
||||
'C': '#f56c6c',
|
||||
'D': '#909399',
|
||||
'-': '#409eff'
|
||||
}
|
||||
return colorMap[params.name] || '#409eff'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.creditChart.setOption(option)
|
||||
},
|
||||
initTypeChart() {
|
||||
this.typeChart = echarts.init(this.$refs.typeChart)
|
||||
const typeData = {}
|
||||
this.supplierList.forEach(supplier => {
|
||||
const type = supplier.type === 'RAW' ? '原料供应商' : '其他供应商'
|
||||
typeData[type] = (typeData[type] || 0) + 1
|
||||
})
|
||||
const data = Object.keys(typeData).map(key => ({
|
||||
name: key,
|
||||
value: typeData[key]
|
||||
}))
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '供应商类型',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
this.typeChart.setOption(option)
|
||||
},
|
||||
initSalesmanCharts() {
|
||||
// 柱状图
|
||||
this.salesmanBarChart = echarts.init(this.$refs.salesmanBarChart)
|
||||
const salesmanData = {}
|
||||
this.orderList.forEach(order => {
|
||||
const salesman = order.salesman || '未知'
|
||||
salesmanData[salesman] = (salesmanData[salesman] || 0) + 1
|
||||
})
|
||||
const salesmanList = Object.keys(salesmanData).sort((a, b) => salesmanData[b] - salesmanData[a])
|
||||
const salesmanCounts = salesmanList.map(s => salesmanData[s])
|
||||
|
||||
const barOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: salesmanList,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单数量',
|
||||
type: 'bar',
|
||||
data: salesmanCounts,
|
||||
itemStyle: {
|
||||
color: '#67c23a'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.salesmanBarChart.setOption(barOption)
|
||||
|
||||
// 饼图
|
||||
this.salesmanPieChart = echarts.init(this.$refs.salesmanPieChart)
|
||||
let pieData = salesmanList.map(name => ({
|
||||
name,
|
||||
value: salesmanData[name]
|
||||
}))
|
||||
pieData = pieData.slice(0, 10)
|
||||
|
||||
const pieOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '业务员订单',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
}
|
||||
this.salesmanPieChart.setOption(pieOption)
|
||||
},
|
||||
initDeptCharts() {
|
||||
// 柱状图
|
||||
this.deptBarChart = echarts.init(this.$refs.deptBarChart)
|
||||
const deptData = {}
|
||||
this.orderList.forEach(order => {
|
||||
const dept = order.deptName || '未知'
|
||||
deptData[dept] = (deptData[dept] || 0) + 1
|
||||
})
|
||||
const deptList = Object.keys(deptData).sort((a, b) => deptData[b] - deptData[a])
|
||||
const deptCounts = deptList.map(d => deptData[d])
|
||||
|
||||
const barOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: deptList,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单数量',
|
||||
type: 'bar',
|
||||
data: deptCounts,
|
||||
itemStyle: {
|
||||
color: '#e6a23c'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.deptBarChart.setOption(barOption)
|
||||
|
||||
// 饼图
|
||||
this.deptPieChart = echarts.init(this.$refs.deptPieChart)
|
||||
let pieData = deptList.map(name => ({
|
||||
name,
|
||||
value: deptData[name]
|
||||
}))
|
||||
pieData = pieData.slice(0, 10)
|
||||
|
||||
const pieOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '部门订单',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
}
|
||||
this.deptPieChart.setOption(pieOption)
|
||||
},
|
||||
initSupplierNameCharts() {
|
||||
// 柱状图
|
||||
this.supplierBarChart = echarts.init(this.$refs.supplierBarChart)
|
||||
const supplierData = {}
|
||||
this.orderList.forEach(order => {
|
||||
const supplier = order.supplierName || '未知'
|
||||
supplierData[supplier] = (supplierData[supplier] || 0) + 1
|
||||
})
|
||||
const supplierList = Object.keys(supplierData).sort((a, b) => supplierData[b] - supplierData[a])
|
||||
const supplierCounts = supplierList.map(s => supplierData[s])
|
||||
|
||||
const barOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: supplierList.slice(0, 15),
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单数量',
|
||||
type: 'bar',
|
||||
data: supplierCounts.slice(0, 15),
|
||||
itemStyle: {
|
||||
color: '#f56c6c'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.supplierBarChart.setOption(barOption)
|
||||
|
||||
// 饼图
|
||||
this.supplierPieChart = echarts.init(this.$refs.supplierPieChart)
|
||||
let pieData = supplierList.map(name => ({
|
||||
name,
|
||||
value: supplierData[name]
|
||||
}))
|
||||
pieData = pieData.slice(0, 10)
|
||||
|
||||
const pieOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '供应商订单',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
}
|
||||
this.supplierPieChart.setOption(pieOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erp-dashboard-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
|
||||
&.supplier-icon {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.order-icon {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.doing-icon {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.complete-icon {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1f2a37;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #7a8694;
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&.full-width {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2a37;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,490 +0,0 @@
|
||||
<template>
|
||||
<div class="erp-order-page">
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>采购订单列表</span>
|
||||
<div class="surface-actions">
|
||||
<el-button type="primary" size="small" @click="openOrderDialog()">新增订单</el-button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="inline-filter">
|
||||
<el-input v-model="orderQuery.orderCode" placeholder="订单编号" size="small" clearable />
|
||||
<el-select
|
||||
v-model="orderQuery.supplierId"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="querySuppliers"
|
||||
:loading="supplierLoading"
|
||||
placeholder="供应商"
|
||||
size="small"
|
||||
clearable
|
||||
@visible-change="handleSupplierDropdown"
|
||||
>
|
||||
<el-option
|
||||
v-for="sp in supplierOptions"
|
||||
:key="sp.supplierId"
|
||||
:label="sp.name"
|
||||
:value="sp.supplierId"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="orderQuery.orderStatus" placeholder="状态" size="small" clearable>
|
||||
<el-option label="草稿" :value="0" />
|
||||
<el-option label="执行中" :value="1" />
|
||||
<el-option label="部分到货" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已取消" :value="4" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="orderRange"
|
||||
type="daterange"
|
||||
unlink-panels
|
||||
value-format="yyyy-MM-dd"
|
||||
start-placeholder="下单开始"
|
||||
end-placeholder="下单结束"
|
||||
size="small"
|
||||
/>
|
||||
<div class="filter-actions">
|
||||
<el-button size="small" type="primary" @click="loadOrders">查询</el-button>
|
||||
<el-button size="small" @click="resetOrderQuery">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="orderList" border stripe highlight-current-row size="small" v-loading="orderLoading">
|
||||
<el-table-column prop="orderCode" label="订单编号" width="140" fixed="left" />
|
||||
<el-table-column prop="supplierName" label="供应商" width="150" />
|
||||
<el-table-column prop="billType" label="单据类型" width="100" />
|
||||
<el-table-column prop="receiveType" label="收发类别" width="100" />
|
||||
<el-table-column prop="deptName" label="部门" width="100" />
|
||||
<el-table-column prop="salesman" label="业务员" width="80" />
|
||||
<el-table-column prop="orderDate" label="下单日期" width="100" />
|
||||
<el-table-column prop="expectedArrival" label="期望到货" width="100" />
|
||||
<el-table-column prop="orderType" label="订单类型" width="100" />
|
||||
<el-table-column prop="totalAmount" label="金额" width="100" />
|
||||
<el-table-column prop="maker" label="制单人" width="80" />
|
||||
<el-table-column prop="auditor" label="审核人" width="80" />
|
||||
<el-table-column prop="bookkeeper" label="记账人" width="80" />
|
||||
<el-table-column prop="auditDate" label="审核日期" width="100" />
|
||||
<el-table-column prop="stockCode" label="存货编码" width="100" />
|
||||
<el-table-column prop="stockName" label="存货名称" width="120" />
|
||||
<el-table-column prop="specModel" label="规格型号" width="100" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="statusTag(scope.row.orderStatus)" size="mini">{{ statusText(scope.row.orderStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="openOrderDialog(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" @click="handleDeleteOrder(scope.row)" style="color:#c0392b">删除</el-button>
|
||||
<!-- <el-divider direction="vertical" />
|
||||
<el-button type="text" size="mini" @click="confirmOrder(scope.row)">下达</el-button>
|
||||
<el-button type="text" size="mini" @click="partialOrder(scope.row)">部分到货</el-button>
|
||||
<el-button type="text" size="mini" @click="completeOrder(scope.row)">完成</el-button>
|
||||
<el-button type="text" size="mini" @click="cancelOrder(scope.row)">取消</el-button> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="orderTotal > 0"
|
||||
:total="orderTotal"
|
||||
:page.sync="orderQuery.pageNum"
|
||||
:limit.sync="orderQuery.pageSize"
|
||||
@pagination="loadOrders"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- 订单弹窗 -->
|
||||
<el-dialog :title="orderDialog.title" :visible.sync="orderDialog.visible" width="1200px" class="order-dialog" @close="closeOrderDialog">
|
||||
<div class="contract-layout">
|
||||
<section class="contract-card">
|
||||
<div class="contract-card__title">基础信息</div>
|
||||
<el-form :model="orderDialog.form" :rules="orderRules" ref="orderForm" label-width="100px" size="small" class="contract-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="订单编号" prop="orderCode">
|
||||
<el-input v-model="orderDialog.form.orderCode" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="单据类型">
|
||||
<el-input v-model="orderDialog.form.billType" placeholder="请输入单据类型" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="收发类别">
|
||||
<el-input v-model="orderDialog.form.receiveType" placeholder="请输入收发类别" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="供应商" prop="supplierId">
|
||||
<el-select
|
||||
v-model="orderDialog.form.supplierId"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="querySuppliers"
|
||||
:loading="supplierLoading"
|
||||
placeholder="请选择供应商"
|
||||
@visible-change="handleSupplierDropdown"
|
||||
>
|
||||
<el-option
|
||||
v-for="sp in supplierOptions"
|
||||
:key="sp.supplierId"
|
||||
:label="sp.name"
|
||||
:value="sp.supplierId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="部门">
|
||||
<el-input v-model="orderDialog.form.deptName" placeholder="请输入部门" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="业务员">
|
||||
<el-input v-model="orderDialog.form.salesman" placeholder="请输入业务员" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="订单类型">
|
||||
<el-input v-model="orderDialog.form.orderType" placeholder="如标准采购、年度合同" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="下单日期" prop="orderDate">
|
||||
<el-date-picker v-model="orderDialog.form.orderDate" type="date" value-format="yyyy-MM-dd" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="期望到货">
|
||||
<el-date-picker v-model="orderDialog.form.expectedArrival" type="date" value-format="yyyy-MM-dd" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="制单人">
|
||||
<el-input v-model="orderDialog.form.maker" placeholder="请输入制单人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="审核人">
|
||||
<el-input v-model="orderDialog.form.auditor" placeholder="请输入审核人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="记账人">
|
||||
<el-input v-model="orderDialog.form.bookkeeper" placeholder="请输入记账人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="审核日期">
|
||||
<el-date-picker v-model="orderDialog.form.auditDate" type="date" value-format="yyyy-MM-dd" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="总金额">
|
||||
<el-input-number v-model="orderDialog.form.totalAmount" :min="0" :precision="2" placeholder="可选" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="存货编码">
|
||||
<el-input v-model="orderDialog.form.stockCode" placeholder="请输入存货编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="存货名称">
|
||||
<el-input v-model="orderDialog.form.stockName" placeholder="请输入存货名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="规格型号">
|
||||
<el-input v-model="orderDialog.form.specModel" placeholder="请输入规格型号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="主计量单位">
|
||||
<el-input v-model="orderDialog.form.mainUnit" placeholder="请输入主计量单位" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="入库数量">
|
||||
<el-input v-model="orderDialog.form.stockQuantity" placeholder="请输入入库数量" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="仓库ID">
|
||||
<WarehouseSelect v-model="orderDialog.form.warehouseId" />
|
||||
<!-- <el-input-number v-model="orderDialog.form.warehouseId" :min="0" placeholder="可选" style="width:100%" /> -->
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注">
|
||||
<el-input type="textarea" v-model="orderDialog.form.remark" placeholder="付款、质保、交付地点等合同条款" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
</div>
|
||||
<div slot="footer" class="contract-footer">
|
||||
<el-button @click="orderDialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitOrder">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listPurchaseOrder,
|
||||
addPurchaseOrder,
|
||||
updatePurchaseOrder,
|
||||
delPurchaseOrder,
|
||||
confirmPurchaseOrder,
|
||||
partialPurchaseOrder,
|
||||
completePurchaseOrder,
|
||||
cancelPurchaseOrder,
|
||||
listSupplier
|
||||
} from '@/api/erp/purchase'
|
||||
import WarehouseSelect from "@/components/KLPService/WarehouseSelect/index.vue"
|
||||
|
||||
export default {
|
||||
name: 'ErpPurchaseOrder',
|
||||
data() {
|
||||
return {
|
||||
orderQuery: { pageNum: 1, pageSize: 10, orderCode: null, supplierId: null, orderStatus: null },
|
||||
orderRange: [],
|
||||
orderList: [],
|
||||
orderTotal: 0,
|
||||
orderLoading: false,
|
||||
orderDialog: { visible: false, title: '', form: {} },
|
||||
orderRules: {
|
||||
orderCode: [{ required: true, message: '请输入订单编号', trigger: 'blur' }],
|
||||
supplierId: [{ required: true, message: '请选择供应商', trigger: 'change' }]
|
||||
},
|
||||
supplierOptions: [],
|
||||
supplierLoading: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
WarehouseSelect,
|
||||
},
|
||||
created() {
|
||||
this.loadOrders()
|
||||
// 默认加载一批供应商作为下拉初始选项
|
||||
this.querySuppliers('')
|
||||
},
|
||||
methods: {
|
||||
loadOrders() {
|
||||
const query = { ...this.orderQuery }
|
||||
if (this.orderRange && this.orderRange.length === 2) {
|
||||
query.beginTime = this.orderRange[0]
|
||||
query.endTime = this.orderRange[1]
|
||||
}
|
||||
this.orderLoading = true
|
||||
listPurchaseOrder(query)
|
||||
.then(res => {
|
||||
this.orderList = res.rows || []
|
||||
this.orderTotal = res.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
this.orderLoading = false
|
||||
})
|
||||
},
|
||||
async querySuppliers(keyword) {
|
||||
this.supplierLoading = true
|
||||
try {
|
||||
const params = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
name: keyword || undefined
|
||||
}
|
||||
const res = await listSupplier(params)
|
||||
this.supplierOptions = res.rows || []
|
||||
} finally {
|
||||
this.supplierLoading = false
|
||||
}
|
||||
},
|
||||
handleSupplierDropdown(visible) {
|
||||
if (visible && !this.supplierOptions.length && !this.supplierLoading) {
|
||||
this.querySuppliers('')
|
||||
}
|
||||
},
|
||||
resetOrderQuery() {
|
||||
this.orderQuery = { pageNum: 1, pageSize: 10, orderCode: null, supplierId: null, orderStatus: null }
|
||||
this.orderRange = []
|
||||
this.loadOrders()
|
||||
},
|
||||
openOrderDialog(row) {
|
||||
if (row) {
|
||||
this.orderDialog.form = { ...row }
|
||||
this.orderDialog.title = '编辑采购订单'
|
||||
} else {
|
||||
this.orderDialog.form = {
|
||||
orderCode: '',
|
||||
supplierId: '',
|
||||
orderDate: '',
|
||||
orderStatus: 0,
|
||||
billType: '',
|
||||
receiveType: '',
|
||||
deptName: '',
|
||||
salesman: '',
|
||||
maker: '',
|
||||
auditor: '',
|
||||
bookkeeper: '',
|
||||
auditDate: '',
|
||||
totalAmount: 0,
|
||||
supplyUnitId: null,
|
||||
stockCode: '',
|
||||
stockName: '',
|
||||
specModel: '',
|
||||
mainUnit: '',
|
||||
stockQuantity: '',
|
||||
warehouseId: null,
|
||||
remark: ''
|
||||
}
|
||||
this.orderDialog.title = '新增采购订单'
|
||||
}
|
||||
this.orderDialog.visible = true
|
||||
this.$nextTick(() => this.$refs.orderForm && this.$refs.orderForm.clearValidate())
|
||||
},
|
||||
closeOrderDialog() {
|
||||
},
|
||||
submitOrder() {
|
||||
this.$refs.orderForm.validate(valid => {
|
||||
if (!valid) return
|
||||
const api = this.orderDialog.form.orderId ? updatePurchaseOrder : addPurchaseOrder
|
||||
api(this.orderDialog.form).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.orderDialog.visible = false
|
||||
this.loadOrders()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDeleteOrder(row) {
|
||||
this.$confirm('确定删除该订单吗?', '提示').then(() => {
|
||||
return delPurchaseOrder(row.orderId)
|
||||
}).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadOrders()
|
||||
})
|
||||
},
|
||||
confirmOrder(row) {
|
||||
confirmPurchaseOrder(row.orderId).then(() => {
|
||||
this.$message.success('已下达')
|
||||
this.loadOrders()
|
||||
})
|
||||
},
|
||||
partialOrder(row) {
|
||||
partialPurchaseOrder(row.orderId).then(() => {
|
||||
this.$message.success('已标记部分到货')
|
||||
this.loadOrders()
|
||||
})
|
||||
},
|
||||
completeOrder(row) {
|
||||
completePurchaseOrder(row.orderId).then(() => {
|
||||
this.$message.success('订单已完成')
|
||||
this.loadOrders()
|
||||
})
|
||||
},
|
||||
cancelOrder(row) {
|
||||
cancelPurchaseOrder(row.orderId).then(() => {
|
||||
this.$message.success('订单已取消')
|
||||
this.loadOrders()
|
||||
})
|
||||
},
|
||||
statusText(status) {
|
||||
switch (status) {
|
||||
case 0: return '草稿'
|
||||
case 1: return '执行中'
|
||||
case 2: return '部分到货'
|
||||
case 3: return '已完成'
|
||||
case 4: return '已取消'
|
||||
default: return '-'
|
||||
}
|
||||
},
|
||||
statusTag(status) {
|
||||
const map = {
|
||||
0: 'info',
|
||||
1: 'primary',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erp-order-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.surface-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
.surface-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #1e2c39;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.surface-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.inline-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
> *:not(.filter-actions) {
|
||||
flex: 1 1 150px;
|
||||
}
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
.order-dialog ::v-deep .el-dialog {
|
||||
max-width: 1200px;
|
||||
}
|
||||
.contract-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.contract-card {
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
.contract-card__title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #1f2d3d;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.contract-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
293
klp-ui/src/views/erp/purchaseAudit/index.vue
Normal file
293
klp-ui/src/views/erp/purchaseAudit/index.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="pa-wb">
|
||||
<div class="pa-main">
|
||||
<!-- 左:计划列表 -->
|
||||
<aside class="pa-aside">
|
||||
<div class="pa-aside-tools">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索计划号 / 供货商 / 合同号"
|
||||
prefix-icon="el-icon-search"
|
||||
@keyup.enter.native="handleQuery"
|
||||
@clear="handleQuery"
|
||||
/>
|
||||
</div>
|
||||
<div class="pa-aside-filter">
|
||||
<el-radio-group v-model="queryParams.auditStatus" size="mini" @change="handleQuery">
|
||||
<el-radio-button label="0">待审</el-radio-button>
|
||||
<el-radio-button label="1">通过</el-radio-button>
|
||||
<el-radio-button label="2">驳回</el-radio-button>
|
||||
<el-radio-button label="">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="pa-pending" v-if="pendingCount">待审 {{ pendingCount }}</span>
|
||||
</div>
|
||||
|
||||
<ul class="pa-list" v-loading="loading">
|
||||
<li
|
||||
v-for="row in planList"
|
||||
:key="row.planId"
|
||||
class="pa-li"
|
||||
:class="{ active: current.planId === row.planId }"
|
||||
@click="selectPlan(row)"
|
||||
>
|
||||
<div class="pa-li-r1">
|
||||
<span class="pa-no">{{ row.planNo }}</span>
|
||||
<span class="pa-badge" :class="'a' + row.auditStatus">{{ auditText(row.auditStatus) }}</span>
|
||||
</div>
|
||||
<div class="pa-li-r2">
|
||||
<span>{{ row.supplier || '—' }}</span>
|
||||
<span class="pa-w">{{ row.planWeight }} T</span>
|
||||
</div>
|
||||
<div class="pa-li-r3">
|
||||
<span>{{ row.purchaseDate || '未排期' }}</span>
|
||||
<span>{{ row.auditor || '' }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!loading && !planList.length" class="pa-empty">暂无数据</li>
|
||||
</ul>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
:pager-count="5"
|
||||
layout="prev, pager, next"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<!-- 右:审核详情 -->
|
||||
<section class="pa-detail">
|
||||
<div v-if="!current.planId" class="pa-placeholder">
|
||||
<i class="el-icon-s-check"></i>
|
||||
<p>从左侧选择一条采购计划进行审核</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="pa-view">
|
||||
<div class="pa-d-head">
|
||||
<div>
|
||||
<span class="pa-d-title">{{ current.planNo }}</span>
|
||||
<span class="pa-badge" :class="'a' + current.auditStatus">{{ auditText(current.auditStatus) }}</span>
|
||||
</div>
|
||||
<div v-if="canAudit" class="pa-actions">
|
||||
<el-button type="primary" size="small" :loading="buttonLoading" @click="doAudit('1')" v-hasPermi="['erp:purchasePlan:audit']">通过</el-button>
|
||||
<el-button type="danger" plain size="small" :loading="buttonLoading" @click="doAudit('2')" v-hasPermi="['erp:purchasePlan:audit']">驳回</el-button>
|
||||
</div>
|
||||
<span v-else-if="current.auditStatus === '2'" class="pa-reaudit">已驳回,待采购方重新送审</span>
|
||||
</div>
|
||||
|
||||
<div class="pa-meta">
|
||||
<div class="pa-meta-i"><label>供货商</label><span>{{ current.supplier || '—' }}</span></div>
|
||||
<div class="pa-meta-i"><label>采购日期</label><span>{{ current.purchaseDate || '—' }}</span></div>
|
||||
<div class="pa-meta-i"><label>计划重量</label><span>{{ current.planWeight }} T</span></div>
|
||||
<div class="pa-meta-i"><label>审核人</label><span>{{ current.auditor || '—' }}</span></div>
|
||||
<div class="pa-meta-i"><label>审核时间</label><span>{{ current.auditTime || '—' }}</span></div>
|
||||
<div class="pa-meta-i"><label>关联合同</label><span>{{ (current.contractCodes || []).join('、') || '—' }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="pa-section">采购明细<span class="pa-section-hint" v-if="current.items && current.items.length">共 {{ current.items.length }} 行</span></div>
|
||||
<el-table :data="current.items || []" border size="mini" max-height="320" class="pa-table">
|
||||
<el-table-column label="#" type="index" width="40" align="center" />
|
||||
<el-table-column label="产品" prop="productType" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="材质" prop="material" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="牌号" prop="grade" width="90" />
|
||||
<el-table-column label="卷号" prop="coilNo" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="宽度" prop="width" width="90" />
|
||||
<el-table-column label="厚度" prop="thickness" width="90" />
|
||||
<el-table-column label="宽公差" prop="widthTolerance" width="80" />
|
||||
<el-table-column label="厚公差" prop="thicknessTolerance" width="80" />
|
||||
<el-table-column label="重量(T)" prop="weight" width="90" align="right" />
|
||||
<el-table-column label="数量" prop="quantity" width="64" align="right" />
|
||||
<el-table-column label="供货商" prop="supplier" width="100" show-overflow-tooltip />
|
||||
<template slot="empty"><span>无明细</span></template>
|
||||
</el-table>
|
||||
|
||||
<div class="pa-section">申请 / 审核意见</div>
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="auditOpinion"
|
||||
:rows="3"
|
||||
:disabled="!canAudit"
|
||||
:placeholder="canAudit ? '请输入审核意见(驳回建议填写理由)' : '无'"
|
||||
class="pa-opinion"
|
||||
/>
|
||||
|
||||
<div class="pa-section" v-if="current.auditLogs && current.auditLogs.length">审核历史</div>
|
||||
<ul class="pa-audit-list" v-if="current.auditLogs && current.auditLogs.length">
|
||||
<li v-for="(log, i) in current.auditLogs" :key="i">
|
||||
<span class="pa-badge" :class="'a' + log.auditStatus">{{ auditText(log.auditStatus) }}</span>
|
||||
<span class="pa-audit-by">{{ log.auditor || '—' }}</span>
|
||||
<span class="pa-audit-time">{{ log.auditTime }}</span>
|
||||
<span class="pa-audit-op">{{ log.auditOpinion || '(无意见)' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPurchasePlan, getPurchasePlan, auditPurchasePlan } from '@/api/erp/purchasePlan'
|
||||
|
||||
export default {
|
||||
name: 'ErpPurchaseAudit',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
buttonLoading: false,
|
||||
total: 0,
|
||||
pendingCount: 0,
|
||||
planList: [],
|
||||
current: {},
|
||||
auditOpinion: '',
|
||||
queryParams: { pageNum: 1, pageSize: 20, keyword: undefined, auditStatus: '0' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 仅「待审核(0)」可审核;驳回后由采购计划方「重新送审」再次进入待审
|
||||
canAudit() {
|
||||
return this.current.auditStatus === '0'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
this.loadPendingCount()
|
||||
},
|
||||
methods: {
|
||||
getList(keepCurrent) {
|
||||
this.loading = true
|
||||
listPurchasePlan(this.queryParams).then(res => {
|
||||
this.planList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.loading = false
|
||||
if (!keepCurrent) {
|
||||
if (this.planList.length) {
|
||||
this.selectPlan(this.planList[0])
|
||||
} else {
|
||||
this.current = {}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadPendingCount() {
|
||||
listPurchasePlan({ pageNum: 1, pageSize: 1, auditStatus: '0' }).then(res => {
|
||||
this.pendingCount = res.total || 0
|
||||
})
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
selectPlan(row) {
|
||||
getPurchasePlan(row.planId).then(res => {
|
||||
this.current = res.data || {}
|
||||
this.auditOpinion = this.current.auditOpinion || ''
|
||||
})
|
||||
},
|
||||
doAudit(status) {
|
||||
this.buttonLoading = true
|
||||
const pid = this.current.planId
|
||||
auditPurchasePlan({
|
||||
planId: pid,
|
||||
auditStatus: status,
|
||||
auditOpinion: this.auditOpinion
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess(status === '1' ? '已通过' : '已驳回')
|
||||
this.getList(true) // 刷新列表但保留右侧当前计划
|
||||
this.loadPendingCount()
|
||||
// 右侧停留在刚审核的计划:状态/历史即时更新,驳回后可直接重新审核
|
||||
getPurchasePlan(pid).then(res => {
|
||||
this.current = res.data || {}
|
||||
this.auditOpinion = ''
|
||||
})
|
||||
}).finally(() => { this.buttonLoading = false })
|
||||
},
|
||||
auditText(s) {
|
||||
return { '0': '待审核', '1': '已通过', '2': '已驳回', '3': '待送审' }[s] || '—'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$accent: #5b8db8;
|
||||
$line: #e4e7ed;
|
||||
$ink: #303133;
|
||||
$sub: #909399;
|
||||
|
||||
.pa-wb {
|
||||
height: calc(100vh - 84px);
|
||||
display: flex; flex-direction: column;
|
||||
background: #f5f6f8; padding: 12px; box-sizing: border-box;
|
||||
}
|
||||
.pa-main { flex: 1; display: flex; gap: 12px; min-height: 0; }
|
||||
.pa-aside-filter { display: flex; align-items: center; justify-content: space-between; }
|
||||
.pa-pending { font-size: 12px; color: #c45656; }
|
||||
|
||||
.pa-aside {
|
||||
width: 300px; flex-shrink: 0; background: #fff; border: 1px solid $line;
|
||||
display: flex; flex-direction: column; min-height: 0;
|
||||
}
|
||||
.pa-aside-tools { padding: 10px; border-bottom: 1px solid $line; }
|
||||
.pa-aside-filter { padding: 8px 10px; border-bottom: 1px solid $line; }
|
||||
.pa-list { flex: 1; overflow-y: auto; margin: 0; padding: 0; list-style: none; }
|
||||
.pa-li {
|
||||
padding: 10px 12px; border-bottom: 1px solid #f0f2f5; cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
&:hover { background: #f7f9fb; }
|
||||
&.active { background: #eef3f8; border-left-color: $accent; }
|
||||
}
|
||||
.pa-li-r1 { display: flex; justify-content: space-between; align-items: center; }
|
||||
.pa-no { font-size: 13px; font-weight: 600; color: $ink; }
|
||||
.pa-li-r2 { display: flex; justify-content: space-between; font-size: 12px; color: $sub; margin: 4px 0; }
|
||||
.pa-w { color: #606266; }
|
||||
.pa-li-r3 { display: flex; justify-content: space-between; font-size: 11px; color: $sub; }
|
||||
.pa-empty { text-align: center; color: $sub; padding: 40px 0; font-size: 13px; }
|
||||
|
||||
.pa-badge {
|
||||
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px;
|
||||
border: 1px solid #dcdfe6; color: $sub; background: #fafafa;
|
||||
&.a1 { color: $accent; border-color: #b9d2e6; background: #eef3f8; }
|
||||
&.a2 { color: #c45656; border-color: #e6c4c4; background: #fbf0f0; }
|
||||
}
|
||||
|
||||
.pa-detail { flex: 1; background: #fff; border: 1px solid $line; min-width: 0; overflow-y: auto; }
|
||||
.pa-placeholder {
|
||||
height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: $sub;
|
||||
i { font-size: 48px; margin-bottom: 12px; color: #d6dce1; }
|
||||
p { font-size: 13px; }
|
||||
}
|
||||
.pa-d-head {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 14px 18px; border-bottom: 1px solid $line;
|
||||
}
|
||||
.pa-d-title { font-size: 15px; font-weight: 600; color: $ink; margin-right: 8px; }
|
||||
.pa-meta {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 24px; padding: 16px 18px;
|
||||
}
|
||||
.pa-meta-i {
|
||||
display: flex; flex-direction: column; font-size: 13px;
|
||||
label { color: $sub; font-size: 12px; margin-bottom: 3px; }
|
||||
span { color: $ink; }
|
||||
}
|
||||
.pa-section {
|
||||
font-size: 13px; font-weight: 600; color: $ink;
|
||||
border-left: 3px solid $accent; padding-left: 8px; margin: 8px 18px 12px;
|
||||
.pa-section-hint { float: right; font-weight: 400; color: $sub; font-size: 12px; }
|
||||
}
|
||||
.pa-table { width: calc(100% - 36px); margin: 0 18px; }
|
||||
.pa-opinion { width: calc(100% - 36px); margin: 0 18px 12px; }
|
||||
.pa-reaudit { font-size: 12px; color: #c45656; margin-right: 6px; }
|
||||
.pa-audit-list { margin: 0 18px 18px; padding: 0; list-style: none; border: 1px solid $line; border-radius: 3px;
|
||||
li { display: flex; align-items: center; gap: 10px; padding: 8px 12px; font-size: 12px; color: #606266; border-bottom: 1px solid #f0f2f5;
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
}
|
||||
.pa-audit-by { color: $ink; }
|
||||
.pa-audit-time { color: $sub; }
|
||||
.pa-audit-op { flex: 1; }
|
||||
</style>
|
||||
894
klp-ui/src/views/erp/purchasePlan/index.vue
Normal file
894
klp-ui/src/views/erp/purchasePlan/index.vue
Normal file
@@ -0,0 +1,894 @@
|
||||
<template>
|
||||
<div class="pp-wb">
|
||||
<div class="pp-main">
|
||||
<!-- 左:计划列表 -->
|
||||
<aside class="pp-col pp-plans">
|
||||
<div class="pp-col-tool">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索计划号 / 供货商 / 合同号"
|
||||
prefix-icon="el-icon-search"
|
||||
@keyup.enter.native="handleQuery"
|
||||
@clear="handleQuery"
|
||||
/>
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd" v-hasPermi="['erp:purchasePlan:add']">新增</el-button>
|
||||
</div>
|
||||
<div class="pp-col-filter">
|
||||
<el-radio-group v-model="queryParams.auditStatus" size="mini" @change="handleQuery">
|
||||
<el-radio-button label="">全部</el-radio-button>
|
||||
<el-radio-button label="3">待送审</el-radio-button>
|
||||
<el-radio-button label="0">待审</el-radio-button>
|
||||
<el-radio-button label="1">通过</el-radio-button>
|
||||
<el-radio-button label="2">驳回</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<ul class="pp-list" v-loading="loading">
|
||||
<li
|
||||
v-for="p in planList"
|
||||
:key="p.planId"
|
||||
class="pp-li"
|
||||
:class="{ active: mode !== 'edit' && current.planId === p.planId }"
|
||||
@click="selectPlan(p)"
|
||||
>
|
||||
<div class="pp-li-r1">
|
||||
<span class="pp-no">{{ p.planNo }}</span>
|
||||
<span class="pp-badge" :class="'a' + p.auditStatus">{{ auditText(p.auditStatus) }}</span>
|
||||
</div>
|
||||
<div class="pp-li-r2">
|
||||
<span>{{ p.supplier || '—' }}</span>
|
||||
<span class="pp-w">{{ p.planWeight }} T</span>
|
||||
</div>
|
||||
<el-progress :percentage="Number(p.progress) || 0" :stroke-width="4" :show-text="false" :color="progressColor" />
|
||||
<div class="pp-li-r3">
|
||||
<span>{{ p.purchaseDate || '未排期' }}</span>
|
||||
<span>{{ (Number(p.progress) || 0).toFixed(0) }}%<i v-if="p.planStatus === '1'" class="pp-arch">已归档</i></span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!loading && !planList.length" class="pp-empty">暂无采购计划</li>
|
||||
</ul>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
:pager-count="5"
|
||||
layout="prev, pager, next"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<!-- 右:详情 / 编辑 -->
|
||||
<section class="pp-col pp-detail">
|
||||
<div v-if="mode === 'empty'" class="pp-placeholder">
|
||||
<i class="el-icon-tickets"></i>
|
||||
<p>从左侧选择一条采购计划,或点击「新增」按销售合同创建</p>
|
||||
</div>
|
||||
|
||||
<!-- 编辑 / 新增 -->
|
||||
<div v-else-if="mode === 'edit'" class="pp-edit">
|
||||
<div class="pp-d-head">
|
||||
<span class="pp-d-title">{{ form.planId ? '编辑采购计划' : '新增采购计划' }}</span>
|
||||
<div>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="buttonLoading" @click="submitForm">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="80px" class="pp-form" size="small">
|
||||
<div class="pp-section">基本信息</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划号" prop="planNo">
|
||||
<el-input v-model="form.planNo" placeholder="留空自动生成" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="采购日期" prop="purchaseDate">
|
||||
<el-date-picker v-model="form.purchaseDate" type="date" value-format="yyyy-MM-dd" style="width:100%" placeholder="选择日期" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="pp-section">
|
||||
销售合同
|
||||
<el-button type="primary" plain size="mini" icon="el-icon-plus" class="pp-section-act" @click="openContractPicker">选择合同</el-button>
|
||||
<span class="pp-section-hint" v-if="form.orderIds.length">已选 {{ form.orderIds.length }} 个合同</span>
|
||||
</div>
|
||||
<div class="pp-picked">
|
||||
<template v-if="selectedContracts.length">
|
||||
<span class="pp-chip" v-for="c in selectedContracts" :key="c.orderId">
|
||||
<b>{{ c.orderCode }}</b><span class="pp-chip-sub">{{ c.contractName }} · {{ c.customer }}</span>
|
||||
<i class="el-icon-close" @click="removeContract(c.orderId)" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="form.contractCodes && form.contractCodes.length">
|
||||
<span class="pp-chip readonly" v-for="code in form.contractCodes" :key="code">{{ code }}</span>
|
||||
<span class="pp-chip-tip">点「选择合同」可修改</span>
|
||||
</template>
|
||||
<span v-else class="pp-picked-empty">未选择销售合同</span>
|
||||
</div>
|
||||
|
||||
<div class="pp-section">
|
||||
采购明细
|
||||
<el-button type="text" size="mini" icon="el-icon-plus" class="pp-section-act" @click="addItem">加行</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-magic-stick" class="pp-section-act" @click="openBatchFill">批量填充</el-button>
|
||||
<span class="pp-section-hint" v-if="form.items.length">共 {{ form.items.length }} 行 · 计 {{ itemsWeight }} T</span>
|
||||
</div>
|
||||
<el-table :data="form.items" border size="mini" max-height="300">
|
||||
<el-table-column label="#" type="index" width="40" align="center" />
|
||||
<el-table-column label="产品" min-width="70">
|
||||
<template slot-scope="s"><el-input v-model="s.row.productType" size="mini" placeholder="热轧卷板" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="材质" min-width="80">
|
||||
<template slot-scope="s"><el-input v-model="s.row.material" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="牌号" min-width="75">
|
||||
<template slot-scope="s"><el-input v-model="s.row.grade" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="卷号" min-width="95">
|
||||
<template slot-scope="s"><el-input v-model="s.row.coilNo" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="宽度" min-width="75">
|
||||
<template slot-scope="s"><el-input v-model="s.row.width" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="厚度" min-width="75">
|
||||
<template slot-scope="s"><el-input v-model="s.row.thickness" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="宽公差" min-width="75">
|
||||
<template slot-scope="s"><el-input v-model="s.row.widthTolerance" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="厚公差" min-width="75">
|
||||
<template slot-scope="s"><el-input v-model="s.row.thicknessTolerance" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重量(T)" min-width="80">
|
||||
<template slot-scope="s"><el-input v-model="s.row.weight" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" min-width="65">
|
||||
<template slot-scope="s"><el-input v-model="s.row.quantity" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="供货商" min-width="90">
|
||||
<template slot-scope="s"><el-input v-model="s.row.supplier" size="mini" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="64" align="center" fixed="right">
|
||||
<template slot-scope="s">
|
||||
<i class="el-icon-document-copy pp-copy" title="复制此行" @click="copyItem(s.$index)" />
|
||||
<i class="el-icon-delete pp-del" title="删除" @click="removeItem(s.$index)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template slot="empty"><span>点「加行」添加,或在上方选择销售合同载入明细</span></template>
|
||||
</el-table>
|
||||
|
||||
<el-form-item label="备注" prop="remark" style="margin-top:14px">
|
||||
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 查看 -->
|
||||
<div v-else class="pp-view">
|
||||
<div class="pp-d-head">
|
||||
<div>
|
||||
<span class="pp-d-title">{{ current.planNo }}</span>
|
||||
<span class="pp-badge" :class="'a' + current.auditStatus">{{ auditText(current.auditStatus) }}</span>
|
||||
<span class="pp-badge plan" :class="current.planStatus === '1' ? 'p1' : 'p0'">{{ current.planStatus === '1' ? '已归档' : '进行中' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="current.auditStatus === '3' || current.auditStatus === '2'"
|
||||
size="small" type="primary" icon="el-icon-s-promotion"
|
||||
:loading="submitLoading" @click="submitForAudit"
|
||||
v-hasPermi="['erp:purchasePlan:edit']"
|
||||
>{{ current.auditStatus === '2' ? '重新送审' : '送审' }}</el-button>
|
||||
<el-button size="small" icon="el-icon-edit" @click="handleEdit" v-hasPermi="['erp:purchasePlan:edit']">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain icon="el-icon-delete" @click="handleDelete(current)" v-hasPermi="['erp:purchasePlan:remove']">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pp-meta">
|
||||
<div class="pp-meta-i"><label>供货商</label><span>{{ current.supplier || '—' }}</span></div>
|
||||
<div class="pp-meta-i"><label>采购日期</label><span>{{ current.purchaseDate || '—' }}</span></div>
|
||||
<div class="pp-meta-i"><label>关联合同</label><span>{{ (current.contractCodes || []).join('、') || '—' }}</span></div>
|
||||
<div class="pp-meta-i"><label>计划重量</label><span>{{ current.planWeight }} T</span></div>
|
||||
<div class="pp-meta-i"><label>已到货</label><span>{{ current.arrivedWeight }} T</span></div>
|
||||
<div class="pp-meta-i wide">
|
||||
<label>到货进度</label>
|
||||
<el-progress
|
||||
:percentage="Number(current.progress) || 0"
|
||||
:stroke-width="10" :text-inside="true" :color="progressColor"
|
||||
:format="p => p.toFixed(1) + '%'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pp-audit" v-if="(current.auditLogs && current.auditLogs.length) || current.auditOpinion">
|
||||
<div class="pp-audit-head">审核记录</div>
|
||||
<ul class="pp-audit-list" v-if="current.auditLogs && current.auditLogs.length">
|
||||
<li v-for="(log, i) in current.auditLogs" :key="i">
|
||||
<span class="pp-badge" :class="'a' + log.auditStatus">{{ auditText(log.auditStatus) }}</span>
|
||||
<span class="pp-audit-by">{{ log.auditor || '—' }}</span>
|
||||
<span class="pp-audit-time">{{ log.auditTime }}</span>
|
||||
<span class="pp-audit-op">{{ log.auditOpinion || '(无意见)' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="pp-audit-op single">
|
||||
<span class="pp-badge" :class="'a' + current.auditStatus">{{ auditText(current.auditStatus) }}</span>
|
||||
{{ current.auditOpinion || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="pp-tabs">
|
||||
<el-tab-pane label="采购明细" name="items">
|
||||
<el-table :data="current.items" border size="mini" max-height="340">
|
||||
<el-table-column label="#" type="index" width="40" align="center" />
|
||||
<el-table-column label="产品" prop="productType" min-width="70" show-overflow-tooltip />
|
||||
<el-table-column label="材质" prop="material" min-width="80" show-overflow-tooltip />
|
||||
<el-table-column label="牌号" prop="grade" min-width="75" />
|
||||
<el-table-column label="卷号" prop="coilNo" min-width="95" show-overflow-tooltip />
|
||||
<el-table-column label="宽度" prop="width" min-width="70" />
|
||||
<el-table-column label="厚度" prop="thickness" min-width="70" />
|
||||
<el-table-column label="重量(T)" prop="weight" min-width="78" align="right" />
|
||||
<el-table-column label="数量" prop="quantity" min-width="60" align="right" />
|
||||
<el-table-column label="已到货(T)" prop="arrivedWeight" min-width="82" align="right" />
|
||||
<el-table-column label="到货状态" min-width="84" align="center">
|
||||
<template slot-scope="s">
|
||||
<span class="pp-istat" :class="'s' + (s.row.itemStatus || '0')">{{ itemStatusText(s.row.itemStatus) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="供货商" prop="supplier" min-width="90" show-overflow-tooltip />
|
||||
<template slot="empty"><span>无明细</span></template>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="到货进度" name="delivery">
|
||||
<div class="pp-deliv-bar">
|
||||
<span class="pp-deliv-tip" v-if="current.auditStatus !== '1'">审核通过后才能上传到货 Excel</span>
|
||||
<el-upload
|
||||
v-else
|
||||
:headers="upload.headers"
|
||||
:action="uploadUrl"
|
||||
:show-file-list="false"
|
||||
accept=".xlsx,.xls"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
>
|
||||
<el-button type="primary" size="small" icon="el-icon-upload2">上传到货 Excel</el-button>
|
||||
</el-upload>
|
||||
<span class="pp-deliv-hint">上传后按牌号+规格回填明细到货量与状态</span>
|
||||
</div>
|
||||
<el-table :data="deliveryList" border stripe size="mini" max-height="340" v-loading="deliveryLoading">
|
||||
<el-table-column label="日期" prop="arrivalDate" width="100" align="center" />
|
||||
<el-table-column label="牌号" prop="grade" width="80" align="center" />
|
||||
<el-table-column label="规格" prop="spec" width="110" align="center" />
|
||||
<el-table-column label="卷号" prop="coilNo" width="110" align="center" />
|
||||
<el-table-column label="单卷(T)" prop="coilWeight" width="80" align="right" />
|
||||
<el-table-column label="车号" prop="truckNo" width="100" align="center" />
|
||||
<el-table-column label="整车(T)" prop="truckWeight" width="80" align="right" />
|
||||
<el-table-column label="件数" prop="pieceCount" width="56" align="center" />
|
||||
<el-table-column label="销售" prop="salesCode" width="85" align="center" />
|
||||
<el-table-column label="到站" prop="arrivalStation" width="90" align="center" />
|
||||
<el-table-column label="" width="36" align="center">
|
||||
<template slot-scope="s"><i class="el-icon-delete pp-del" @click="removeDelivery(s.row)" /></template>
|
||||
</el-table-column>
|
||||
<template slot="empty"><span>暂无到货记录</span></template>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 选择销售合同:完整信息表格 + 多选 -->
|
||||
<el-dialog title="选择销售合同" :visible.sync="pickerOpen" width="900px" append-to-body>
|
||||
<div class="pp-picker-tool">
|
||||
<el-input
|
||||
v-model="pickerQuery.keyword"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索合同号 / 合同名称 / 客户"
|
||||
prefix-icon="el-icon-search"
|
||||
style="width:280px"
|
||||
@keyup.enter.native="reloadPicker"
|
||||
@clear="reloadPicker"
|
||||
/>
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="reloadPicker">查询</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
ref="pickerTable"
|
||||
:data="pickerList"
|
||||
v-loading="pickerLoading"
|
||||
border size="small"
|
||||
max-height="420"
|
||||
@selection-change="onPickerSelection"
|
||||
row-key="orderId"
|
||||
>
|
||||
<el-table-column type="selection" width="46" reserve-selection />
|
||||
<el-table-column label="订单编号" prop="orderCode" width="150" show-overflow-tooltip />
|
||||
<el-table-column label="合同号" prop="contractCode" width="130" show-overflow-tooltip />
|
||||
<el-table-column label="合同名称" prop="contractName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="客户(需方)" prop="customer" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="销售员" prop="salesman" width="90" />
|
||||
<el-table-column label="金额" prop="orderAmount" width="100" align="right" />
|
||||
<el-table-column label="已有计划" prop="planCount" width="80" align="center">
|
||||
<template slot-scope="s"><span :class="{ 'pp-cnt-zero': !s.row.planCount }">{{ s.row.planCount || 0 }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="pickerTotal > 0"
|
||||
:total="pickerTotal"
|
||||
:page.sync="pickerQuery.pageNum"
|
||||
:limit.sync="pickerQuery.pageSize"
|
||||
@pagination="loadPicker"
|
||||
/>
|
||||
<div slot="footer">
|
||||
<span class="pp-picker-count">已选 {{ pickerSelection.length }} 个合同</span>
|
||||
<el-button @click="pickerOpen = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmPicker">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选择供应商:表格 + 单选 -->
|
||||
<el-dialog title="选择供应商" :visible.sync="supplierPickerOpen" width="760px" append-to-body>
|
||||
<div class="pp-picker-tool">
|
||||
<el-input
|
||||
v-model="supplierQuery.name"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索供应商名称"
|
||||
prefix-icon="el-icon-search"
|
||||
style="width:260px"
|
||||
@keyup.enter.native="reloadSuppliers"
|
||||
@clear="reloadSuppliers"
|
||||
/>
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="reloadSuppliers">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="supplierList" v-loading="supplierLoading" border size="small" max-height="420" @row-click="chooseSupplier">
|
||||
<el-table-column label="编码" prop="supplierCode" width="130" show-overflow-tooltip />
|
||||
<el-table-column label="供应商名称" prop="name" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="类型" prop="type" width="90" align="center" />
|
||||
<el-table-column label="信用" prop="creditRating" width="70" align="center" />
|
||||
<el-table-column label="联系人" prop="contactPerson" width="100" />
|
||||
<el-table-column label="电话" prop="contactPhone" width="130" />
|
||||
<el-table-column label="操作" width="70" align="center">
|
||||
<template slot-scope="s"><el-button type="text" size="mini" @click.stop="chooseSupplier(s.row)">选择</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="supplierTotal > 0"
|
||||
:total="supplierTotal"
|
||||
:page.sync="supplierQuery.pageNum"
|
||||
:limit.sync="supplierQuery.pageSize"
|
||||
@pagination="loadSuppliers"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量填充明细:设一次,一键填满所有行(留空不覆盖) -->
|
||||
<el-dialog title="批量填充明细" :visible.sync="batchFillOpen" width="560px" append-to-body>
|
||||
<p class="pp-batch-tip">下面填了值的字段会写入<b>全部明细行</b>,留空的不覆盖。常用于整批同产品/材质/规格。</p>
|
||||
<el-form :model="batchFill" label-width="72px" size="small">
|
||||
<el-row :gutter="14">
|
||||
<el-col :span="12"><el-form-item label="产品"><el-input v-model="batchFill.productType" placeholder="如 热轧卷板" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="材质"><el-input v-model="batchFill.material" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="牌号"><el-input v-model="batchFill.grade" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="卷号"><el-input v-model="batchFill.coilNo" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="宽度"><el-input v-model="batchFill.width" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="厚度"><el-input v-model="batchFill.thickness" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="宽公差"><el-input v-model="batchFill.widthTolerance" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="厚公差"><el-input v-model="batchFill.thicknessTolerance" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="重量(T)"><el-input v-model="batchFill.weight" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="数量"><el-input v-model="batchFill.quantity" /></el-form-item></el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="供货商">
|
||||
<div class="pp-inline-pick">
|
||||
<el-input v-model="batchFill.supplier" placeholder="选择或输入" />
|
||||
<el-button icon="el-icon-search" @click="openSupplierPicker">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="pp-batch-footer">
|
||||
<el-checkbox v-model="batchFillNewRow">设为「加行」默认值</el-checkbox>
|
||||
<span class="pp-batch-gen">生成 <el-input v-model="batchRows" size="mini" style="width:64px" /> 行</span>
|
||||
<el-button @click="batchFillOpen = false">取消</el-button>
|
||||
<el-button @click="applyBatchFill">应用到现有行</el-button>
|
||||
<el-button type="primary" @click="generateRows">生成行并填充</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listPurchasePlan,
|
||||
purchasePlanStatistics,
|
||||
getPurchasePlan,
|
||||
getItemsByOrders,
|
||||
addPurchasePlan,
|
||||
updatePurchasePlan,
|
||||
delPurchasePlan,
|
||||
submitPurchasePlan,
|
||||
listDelivery,
|
||||
delDelivery,
|
||||
listContracts
|
||||
} from '@/api/erp/purchasePlan'
|
||||
import { listSupplier } from '@/api/erp/purchase'
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'ErpPurchasePlan',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
total: 0,
|
||||
planList: [],
|
||||
stats: { total: 0, completed: 0 },
|
||||
queryParams: { pageNum: 1, pageSize: 20, keyword: undefined, auditStatus: '' },
|
||||
mode: 'empty', // empty | view | edit
|
||||
current: {},
|
||||
activeTab: 'items',
|
||||
form: {},
|
||||
rules: {},
|
||||
buttonLoading: false,
|
||||
submitLoading: false,
|
||||
selectedContracts: [],
|
||||
// 批量填充
|
||||
batchFillOpen: false,
|
||||
batchFillNewRow: true,
|
||||
batchRows: 0,
|
||||
batchFill: { productType: '热轧卷板', material: '', grade: '', coilNo: '', width: '', thickness: '', widthTolerance: '', thicknessTolerance: '', weight: '', quantity: '', supplier: '' },
|
||||
newRowDefaults: null,
|
||||
// 合同选择器
|
||||
pickerOpen: false,
|
||||
pickerLoading: false,
|
||||
pickerList: [],
|
||||
pickerTotal: 0,
|
||||
pickerQuery: { pageNum: 1, pageSize: 10, keyword: undefined },
|
||||
pickerSelection: [],
|
||||
// 供应商选择器
|
||||
supplierPickerOpen: false,
|
||||
supplierLoading: false,
|
||||
supplierList: [],
|
||||
supplierTotal: 0,
|
||||
supplierQuery: { pageNum: 1, pageSize: 10, name: undefined },
|
||||
// 到货
|
||||
deliveryList: [],
|
||||
deliveryLoading: false,
|
||||
upload: { headers: { Authorization: 'Bearer ' + getToken() } },
|
||||
progressColor: '#5b8db8'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uploadUrl() {
|
||||
return process.env.VUE_APP_BASE_API + '/erp/purchasePlan/' + (this.current.planId || '') + '/importDelivery'
|
||||
},
|
||||
itemsWeight() {
|
||||
return (this.form.items || []).reduce((s, i) => s + (Number(i.weight) || 0), 0).toFixed(3)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
getList(keepCurrent) {
|
||||
this.loading = true
|
||||
listPurchasePlan(this.queryParams).then(res => {
|
||||
this.planList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.loading = false
|
||||
if (!keepCurrent && this.mode !== 'edit') {
|
||||
if (this.planList.length) this.selectPlan(this.planList[0])
|
||||
else { this.mode = 'empty'; this.current = {} }
|
||||
}
|
||||
})
|
||||
purchasePlanStatistics(this.queryParams).then(res => { this.stats = res.data || { total: 0, completed: 0 } })
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
selectPlan(p) {
|
||||
this.activeTab = 'items'
|
||||
this.mode = 'view'
|
||||
this.current = { ...p }
|
||||
this.refreshDetail()
|
||||
},
|
||||
refreshDetail() {
|
||||
const planId = this.current.planId
|
||||
if (!planId) return
|
||||
getPurchasePlan(planId).then(res => { this.current = { ...this.current, ...(res.data || {}) } })
|
||||
this.deliveryLoading = true
|
||||
listDelivery(planId).then(res => { this.deliveryList = res.data || [] }).finally(() => { this.deliveryLoading = false })
|
||||
},
|
||||
// ---- 新增 / 编辑 ----
|
||||
resetForm() {
|
||||
this.form = { planId: null, planNo: '', supplier: '', purchaseDate: '', remark: '', items: [], orderIds: [], contractCodes: [] }
|
||||
this.selectedContracts = []
|
||||
this.newRowDefaults = null
|
||||
this.batchFill = { productType: '热轧卷板', material: '', grade: '', coilNo: '', width: '', thickness: '', widthTolerance: '', thicknessTolerance: '', weight: '', quantity: '', supplier: '' }
|
||||
this.batchRows = 0
|
||||
},
|
||||
handleAdd() {
|
||||
this.resetForm()
|
||||
this.mode = 'edit'
|
||||
},
|
||||
handleEdit() {
|
||||
getPurchasePlan(this.current.planId).then(res => {
|
||||
const d = res.data || {}
|
||||
this.form = {
|
||||
planId: d.planId, planNo: d.planNo, supplier: d.supplier,
|
||||
purchaseDate: d.purchaseDate, remark: d.remark,
|
||||
items: d.items || [], orderIds: d.orderIds || [], contractCodes: d.contractCodes || []
|
||||
}
|
||||
this.selectedContracts = []
|
||||
this.mode = 'edit'
|
||||
})
|
||||
},
|
||||
cancelEdit() {
|
||||
this.mode = this.planList.length ? 'view' : 'empty'
|
||||
if (this.planList.length) {
|
||||
const back = this.current.planId ? this.planList.find(p => p.planId === this.current.planId) : this.planList[0]
|
||||
this.selectPlan(back || this.planList[0])
|
||||
}
|
||||
},
|
||||
// ---- 合同选择器 ----
|
||||
openContractPicker() {
|
||||
this.pickerQuery.pageNum = 1
|
||||
this.loadPicker()
|
||||
this.pickerOpen = true
|
||||
},
|
||||
reloadPicker() {
|
||||
this.pickerQuery.pageNum = 1
|
||||
this.loadPicker()
|
||||
},
|
||||
loadPicker() {
|
||||
this.pickerLoading = true
|
||||
listContracts(this.pickerQuery).then(res => {
|
||||
this.pickerList = res.rows || []
|
||||
this.pickerTotal = res.total || 0
|
||||
this.pickerLoading = false
|
||||
// 回显已选
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.pickerTable) return
|
||||
this.pickerList.forEach(row => {
|
||||
if (this.form.orderIds.includes(row.orderId)) {
|
||||
this.$refs.pickerTable.toggleRowSelection(row, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
onPickerSelection(sel) {
|
||||
this.pickerSelection = sel
|
||||
},
|
||||
confirmPicker() {
|
||||
this.selectedContracts = this.pickerSelection.map(c => ({
|
||||
orderId: c.orderId, orderCode: c.orderCode, contractName: c.contractName, customer: c.customer
|
||||
}))
|
||||
this.form.orderIds = this.pickerSelection.map(c => c.orderId)
|
||||
this.form.contractCodes = []
|
||||
this.pickerOpen = false
|
||||
this.deriveItems(this.form.orderIds)
|
||||
},
|
||||
removeContract(orderId) {
|
||||
this.selectedContracts = this.selectedContracts.filter(c => c.orderId !== orderId)
|
||||
this.form.orderIds = this.form.orderIds.filter(id => id !== orderId)
|
||||
this.deriveItems(this.form.orderIds)
|
||||
},
|
||||
// ---- 供应商选择器 ----
|
||||
openSupplierPicker() {
|
||||
this.supplierQuery.pageNum = 1
|
||||
this.loadSuppliers()
|
||||
this.supplierPickerOpen = true
|
||||
},
|
||||
reloadSuppliers() {
|
||||
this.supplierQuery.pageNum = 1
|
||||
this.loadSuppliers()
|
||||
},
|
||||
loadSuppliers() {
|
||||
this.supplierLoading = true
|
||||
listSupplier(this.supplierQuery).then(res => {
|
||||
this.supplierList = res.rows || []
|
||||
this.supplierTotal = res.total || 0
|
||||
this.supplierLoading = false
|
||||
})
|
||||
},
|
||||
chooseSupplier(row) {
|
||||
// 供应商选择器服务于「批量填充」对话框
|
||||
this.batchFill.supplier = row.name
|
||||
this.supplierPickerOpen = false
|
||||
},
|
||||
deriveItems(orderIds) {
|
||||
if (!orderIds || !orderIds.length) { this.form.items = []; return }
|
||||
getItemsByOrders(orderIds).then(res => {
|
||||
this.form.items = (res.data || []).map(it => ({
|
||||
productType: it.productType || '热轧卷板',
|
||||
material: it.material || '',
|
||||
grade: it.grade || '',
|
||||
coilNo: '',
|
||||
width: it.width || '',
|
||||
thickness: it.thickness || '',
|
||||
widthTolerance: it.widthTolerance || '0',
|
||||
thicknessTolerance: it.thicknessTolerance || '0',
|
||||
weight: it.weight,
|
||||
quantity: it.quantity,
|
||||
supplier: this.form.supplier || ''
|
||||
}))
|
||||
this.$modal.msgSuccess(`已载入 ${this.form.items.length} 条明细,可继续编辑`)
|
||||
})
|
||||
},
|
||||
blankItem() {
|
||||
return { productType: '热轧卷板', material: '', grade: '', coilNo: '', width: '', thickness: '', widthTolerance: '0', thicknessTolerance: '0', weight: '', quantity: '', supplier: '' }
|
||||
},
|
||||
addItem() {
|
||||
// 优先用批量默认值,其次继承上一行,最后空行;重量每行不同故清空
|
||||
const last = this.form.items[this.form.items.length - 1]
|
||||
let row
|
||||
if (this.newRowDefaults) row = { ...this.newRowDefaults }
|
||||
else if (last) row = { ...last }
|
||||
else row = this.blankItem()
|
||||
row.weight = ''
|
||||
this.form.items.push(row)
|
||||
},
|
||||
copyItem(index) {
|
||||
this.form.items.splice(index + 1, 0, { ...this.form.items[index] })
|
||||
},
|
||||
removeItem(index) {
|
||||
this.form.items.splice(index, 1)
|
||||
},
|
||||
openBatchFill() {
|
||||
this.batchFillOpen = true
|
||||
},
|
||||
batchFillKeys() {
|
||||
const f = this.batchFill
|
||||
const keys = ['productType', 'material', 'grade', 'coilNo', 'width', 'thickness', 'widthTolerance', 'thicknessTolerance', 'weight', 'quantity', 'supplier']
|
||||
return keys.filter(k => f[k] !== '' && f[k] != null)
|
||||
},
|
||||
applyBatchFill() {
|
||||
const f = this.batchFill
|
||||
const filled = this.batchFillKeys()
|
||||
if (!filled.length) { this.$modal.msgWarning('请至少填写一个要填充的字段'); return }
|
||||
if (!this.form.items.length) { this.$modal.msgWarning('当前没有明细行,请用右侧「生成行并填充」'); return }
|
||||
this.form.items.forEach(it => { filled.forEach(k => { it[k] = f[k] }) })
|
||||
if (f.supplier) this.form.supplier = f.supplier
|
||||
this.saveNewRowDefaults(filled)
|
||||
this.batchFillOpen = false
|
||||
this.$modal.msgSuccess(`已填充 ${this.form.items.length} 行`)
|
||||
},
|
||||
generateRows() {
|
||||
const n = parseInt(this.batchRows, 10) || 0
|
||||
if (n <= 0) { this.$modal.msgWarning('请填写要生成的行数'); return }
|
||||
const f = this.batchFill
|
||||
const filled = this.batchFillKeys()
|
||||
for (let i = 0; i < n; i++) {
|
||||
const row = this.blankItem()
|
||||
filled.forEach(k => { row[k] = f[k] })
|
||||
this.form.items.push(row)
|
||||
}
|
||||
if (f.supplier) this.form.supplier = f.supplier
|
||||
this.saveNewRowDefaults(filled)
|
||||
this.batchFillOpen = false
|
||||
this.$modal.msgSuccess(`已生成 ${n} 行`)
|
||||
},
|
||||
saveNewRowDefaults(filled) {
|
||||
if (!this.batchFillNewRow) return
|
||||
const def = this.blankItem()
|
||||
filled.forEach(k => { def[k] = this.batchFill[k] })
|
||||
this.newRowDefaults = def
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs['form'].validate(valid => {
|
||||
if (!valid) return
|
||||
if (!this.form.orderIds.length) { this.$modal.msgWarning('请先选择销售合同'); return }
|
||||
this.buttonLoading = true
|
||||
const api = this.form.planId ? updatePurchasePlan : addPurchasePlan
|
||||
const editedId = this.form.planId
|
||||
api(this.form).then(() => {
|
||||
this.$modal.msgSuccess('保存成功')
|
||||
this.mode = 'view'
|
||||
this.loading = true
|
||||
listPurchasePlan(this.queryParams).then(res => {
|
||||
this.planList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.loading = false
|
||||
const target = editedId ? this.planList.find(p => p.planId === editedId) : this.planList[0]
|
||||
if (target) this.selectPlan(target)
|
||||
})
|
||||
purchasePlanStatistics(this.queryParams).then(res => { this.stats = res.data || { total: 0, completed: 0 } })
|
||||
}).finally(() => { this.buttonLoading = false })
|
||||
})
|
||||
},
|
||||
submitForAudit() {
|
||||
const tip = this.current.auditStatus === '2' ? '确认重新送审该计划?将再次进入审核' : '确认送审该计划?送审后将出现在采购审核页'
|
||||
this.$modal.confirm(tip).then(() => {
|
||||
this.submitLoading = true
|
||||
return submitPurchasePlan(this.current.planId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('已送审')
|
||||
this.getList(true)
|
||||
this.refreshDetail()
|
||||
}).catch(() => {}).finally(() => { this.submitLoading = false })
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm('确认删除采购计划「' + row.planNo + '」?').then(() => {
|
||||
return delPurchasePlan(row.planId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.mode = 'empty'
|
||||
this.current = {}
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
// ---- 到货 ----
|
||||
handleUploadSuccess(res) {
|
||||
if (res.code === 200) {
|
||||
const data = res.data || {}
|
||||
if (data.kgConverted) {
|
||||
this.$alert(res.msg, '导入完成(含单位纠正)', { dangerouslyUseHTMLString: true, type: 'warning' })
|
||||
} else {
|
||||
this.$modal.msgSuccess(res.msg || '导入成功')
|
||||
}
|
||||
this.refreshDetail()
|
||||
this.getList(true)
|
||||
} else {
|
||||
this.$alert(res.msg || '导入失败', '到货文件校验未通过', { dangerouslyUseHTMLString: true, type: 'error' })
|
||||
}
|
||||
},
|
||||
handleUploadError() {
|
||||
this.$modal.msgError('上传失败,请检查文件后重试')
|
||||
},
|
||||
removeDelivery(row) {
|
||||
this.$modal.confirm('确定删除该到货记录吗?').then(() => {
|
||||
return delDelivery(row.deliveryId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.refreshDetail()
|
||||
this.getList(true)
|
||||
}).catch(() => {})
|
||||
},
|
||||
auditText(s) {
|
||||
return { '0': '待审核', '1': '已通过', '2': '已驳回', '3': '待送审' }[s] || '—'
|
||||
},
|
||||
itemStatusText(s) {
|
||||
return { '0': '未到货', '1': '部分到货', '2': '已到货' }[s] || '未到货'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$accent: #5b8db8;
|
||||
$line: #e4e7ed;
|
||||
$ink: #303133;
|
||||
$sub: #909399;
|
||||
|
||||
.pp-wb {
|
||||
height: calc(100vh - 84px);
|
||||
display: flex; flex-direction: column;
|
||||
background: #f5f6f8; padding: 12px; box-sizing: border-box;
|
||||
}
|
||||
.pp-main { flex: 1; display: flex; gap: 12px; min-height: 0; }
|
||||
|
||||
.pp-col { background: #fff; border: 1px solid $line; display: flex; flex-direction: column; min-height: 0; }
|
||||
.pp-plans { width: 300px; flex-shrink: 0; }
|
||||
.pp-detail { flex: 1; min-width: 0; overflow-y: auto; }
|
||||
|
||||
.pp-col-tool {
|
||||
padding: 10px; border-bottom: 1px solid $line;
|
||||
display: flex; gap: 8px; align-items: center;
|
||||
|
||||
// 仅对齐,不改高度(沿用全局 24px)
|
||||
::v-deep .el-input { flex: 1; }
|
||||
::v-deep .el-button { margin: 0; flex-shrink: 0; }
|
||||
}
|
||||
|
||||
// 供货商:输入框 + 选择按钮 并排,等高对齐
|
||||
.pp-inline-pick {
|
||||
display: flex; gap: 8px; align-items: center;
|
||||
::v-deep .el-input { flex: 1; }
|
||||
::v-deep .el-button { flex-shrink: 0; margin: 0; }
|
||||
}
|
||||
.pp-col-filter { padding: 8px 10px; border-bottom: 1px solid $line; }
|
||||
|
||||
.pp-list { flex: 1; overflow-y: auto; margin: 0; padding: 0; list-style: none; }
|
||||
.pp-li {
|
||||
padding: 10px 12px; border-bottom: 1px solid #f0f2f5; cursor: pointer; border-left: 3px solid transparent;
|
||||
&:hover { background: #f7f9fb; }
|
||||
&.active { background: #eef3f8; border-left-color: $accent; }
|
||||
}
|
||||
.pp-li-r1 { display: flex; justify-content: space-between; align-items: center; }
|
||||
.pp-no { font-size: 13px; font-weight: 600; color: $ink; }
|
||||
.pp-li-r2 { display: flex; justify-content: space-between; font-size: 12px; color: $sub; margin: 4px 0 6px; }
|
||||
.pp-w { color: #606266; }
|
||||
.pp-li-r3 { display: flex; justify-content: space-between; font-size: 11px; color: $sub; margin-top: 4px; }
|
||||
.pp-arch { font-style: normal; color: $accent; margin-left: 6px; }
|
||||
.pp-empty { text-align: center; color: $sub; padding: 36px 12px; font-size: 13px; }
|
||||
|
||||
.pp-badge {
|
||||
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px;
|
||||
border: 1px solid #dcdfe6; color: $sub; background: #fafafa;
|
||||
&.a1 { color: $accent; border-color: #b9d2e6; background: #eef3f8; }
|
||||
&.a2 { color: #c45656; border-color: #e6c4c4; background: #fbf0f0; }
|
||||
&.a3 { color: #d6a256; border-color: #ecd4a6; background: #fdf6ec; }
|
||||
&.plan { margin-left: 6px; }
|
||||
&.p1 { color: $accent; border-color: #b9d2e6; }
|
||||
}
|
||||
|
||||
.pp-placeholder {
|
||||
height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: $sub;
|
||||
i { font-size: 46px; margin-bottom: 12px; color: #d6dce1; }
|
||||
p { font-size: 13px; }
|
||||
}
|
||||
.pp-d-head {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 14px 18px; border-bottom: 1px solid $line;
|
||||
}
|
||||
.pp-d-title { font-size: 15px; font-weight: 600; color: $ink; margin-right: 8px; }
|
||||
.pp-meta { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 24px; padding: 16px 18px; }
|
||||
.pp-meta-i {
|
||||
display: flex; flex-direction: column; font-size: 13px;
|
||||
label { color: $sub; font-size: 12px; margin-bottom: 3px; }
|
||||
span { color: $ink; }
|
||||
&.wide { grid-column: span 3; }
|
||||
}
|
||||
.pp-tabs { padding: 0 18px 18px; }
|
||||
|
||||
.pp-edit { padding: 0 0 18px; }
|
||||
.pp-form { padding: 0 18px; }
|
||||
.pp-section {
|
||||
font-size: 13px; font-weight: 600; color: $ink;
|
||||
border-left: 3px solid $accent; padding-left: 8px; margin: 16px 0 12px;
|
||||
.pp-section-act { margin-left: 10px; }
|
||||
.pp-section-hint { float: right; font-weight: 400; color: $sub; font-size: 12px; }
|
||||
}
|
||||
|
||||
/* 已选合同 chips */
|
||||
.pp-picked { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 4px; min-height: 24px; }
|
||||
.pp-chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
border: 1px solid #b9d2e6; background: #eef3f8; color: $accent;
|
||||
border-radius: 3px; padding: 3px 8px; font-size: 12px;
|
||||
b { font-weight: 600; }
|
||||
.pp-chip-sub { color: $sub; font-weight: 400; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.el-icon-close { cursor: pointer; color: $sub; &:hover { color: #c45656; } }
|
||||
&.readonly { color: #606266; border-color: #dcdfe6; background: #fafafa; }
|
||||
}
|
||||
.pp-chip-tip { color: $sub; font-size: 12px; align-self: center; }
|
||||
.pp-picked-empty { color: $sub; font-size: 13px; }
|
||||
|
||||
.pp-deliv-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; min-height: 32px; }
|
||||
.pp-deliv-tip { color: $sub; font-size: 13px; }
|
||||
.pp-deliv-hint { color: $sub; font-size: 12px; }
|
||||
.pp-del { color: #c45656; cursor: pointer; }
|
||||
.pp-copy { color: #5b8db8; cursor: pointer; margin-right: 10px; }
|
||||
.pp-batch-tip { font-size: 12px; color: #909399; margin: 0 0 14px; line-height: 1.6; b { color: #5b8db8; } }
|
||||
.pp-batch-footer { display: flex; align-items: center; gap: 10px;
|
||||
.el-checkbox { margin-right: auto; }
|
||||
.pp-batch-gen { font-size: 13px; color: #606266; }
|
||||
}
|
||||
|
||||
/* 审核记录 */
|
||||
.pp-audit { margin: 0 18px 14px; border: 1px solid $line; border-radius: 3px; }
|
||||
.pp-audit-head { font-size: 13px; font-weight: 600; color: $ink; padding: 8px 12px; border-bottom: 1px solid $line; background: #fafbfc; }
|
||||
.pp-audit-list { margin: 0; padding: 0; list-style: none;
|
||||
li { display: flex; align-items: center; gap: 10px; padding: 8px 12px; font-size: 12px; color: #606266; border-bottom: 1px solid #f0f2f5;
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
}
|
||||
.pp-audit-by { color: $ink; }
|
||||
.pp-audit-time { color: $sub; }
|
||||
.pp-audit-op { flex: 1; color: #606266;
|
||||
&.single { padding: 10px 12px; }
|
||||
}
|
||||
|
||||
.pp-istat {
|
||||
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px; border: 1px solid #dcdfe6; color: $sub;
|
||||
&.s1 { color: #d6a256; border-color: #ecd4a6; background: #fdf6ec; }
|
||||
&.s2 { color: $accent; border-color: #b9d2e6; background: #eef3f8; }
|
||||
}
|
||||
|
||||
/* 合同选择器 */
|
||||
.pp-picker-tool { display: flex; gap: 8px; margin-bottom: 10px; }
|
||||
.pp-picker-count { float: left; color: $sub; font-size: 13px; line-height: 32px; }
|
||||
.pp-cnt-zero { color: #c0c4cc; }
|
||||
</style>
|
||||
@@ -1,238 +0,0 @@
|
||||
<template>
|
||||
<div class="erp-receipt-page">
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>收货记录</span>
|
||||
<el-button type="primary" size="small" @click="openDialog()">新增收货</el-button>
|
||||
</header>
|
||||
<div class="inline-filter">
|
||||
<!-- <el-input v-model="query.orderId" placeholder="订单ID" size="small" clearable /> -->
|
||||
<OrderSelect v-model="query.orderId" placeholder="订单号" size="small" />
|
||||
<!-- <el-input v-model="query.itemId" placeholder="订单明细ID" size="small" clearable /> -->
|
||||
<OrderDetailSelect :orderId="query.orderId" v-model="query.itemId"></OrderDetailSelect>
|
||||
<el-select v-model="query.quality" placeholder="质检结果" size="small" clearable>
|
||||
<el-option label="合格" value="OK" />
|
||||
<el-option label="不合格" value="NG" />
|
||||
</el-select>
|
||||
<div class="filter-actions">
|
||||
<el-button size="small" type="primary" @click="loadData">查询</el-button>
|
||||
<el-button size="small" @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="list" border size="small" v-loading="loading">
|
||||
<el-table-column prop="orderCode" label="订单编号" />
|
||||
<el-table-column prop="itemId" label="明细编号" />
|
||||
<el-table-column prop="receivedQty" label="收货数量" />
|
||||
<el-table-column prop="qualityResult" label="质检结果">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.qualityResult === 'NG' ? 'danger' : 'success'" size="mini">
|
||||
{{ scope.row.qualityResult || 'OK' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receiptTime" label="收货时间" />
|
||||
<el-table-column label="备注" prop="remark" />
|
||||
<el-table-column label="操作" width="140">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="openDialog(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" class="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="query.pageNum"
|
||||
:limit.sync="query.pageSize"
|
||||
@pagination="loadData"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>到货趋势</span>
|
||||
</header>
|
||||
<el-table :data="chartData" border size="small" height="300">
|
||||
<el-table-column prop="date" label="日期" />
|
||||
<el-table-column prop="totalQty" label="累计收货(吨)" />
|
||||
<el-table-column prop="qualifiedQty" label="合格数量(吨)" />
|
||||
<el-table-column prop="ngQty" label="不合格(吨)" />
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<el-dialog :title="dialog.title" :visible.sync="dialog.visible" width="480px">
|
||||
<el-form :model="dialog.form" :rules="rules" ref="form" label-width="100px" size="small">
|
||||
<el-form-item label="订单ID" prop="orderId">
|
||||
<!-- <el-input v-model="dialog.form.orderId" /> -->
|
||||
<OrderSelect v-model="dialog.form.orderId" placeholder="订单号" size="small" />
|
||||
</el-form-item>
|
||||
<el-form-item label="明细ID" prop="itemId">
|
||||
<!-- <el-input v-model="dialog.form.itemId" /> -->
|
||||
<OrderDetailSelect :orderId="dialog.form.orderId" v-model="dialog.form.itemId"></OrderDetailSelect>
|
||||
</el-form-item>
|
||||
<el-form-item label="收货数量" prop="receivedQty">
|
||||
<el-input-number v-model="dialog.form.receivedQty" :min="0" :precision="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="质检结果">
|
||||
<el-select v-model="dialog.form.qualityResult" clearable>
|
||||
<el-option label="合格" value="OK" />
|
||||
<el-option label="不合格" value="NG" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="收货时间">
|
||||
<el-date-picker v-model="dialog.form.receiptTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input type="textarea" v-model="dialog.form.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submit">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listPurchaseReceipt,
|
||||
addPurchaseReceipt,
|
||||
updatePurchaseReceipt,
|
||||
delPurchaseReceipt
|
||||
} from '@/api/erp/purchase'
|
||||
import OrderSelect from '@/components/KLPService/OrderSelect'
|
||||
import OrderDetailSelect from '@/components/KLPService/OrderDetailSelect'
|
||||
|
||||
export default {
|
||||
name: 'ErpPurchaseReceipt',
|
||||
components: {
|
||||
OrderSelect,
|
||||
OrderDetailSelect
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: { pageNum: 1, pageSize: 10, orderId: null, itemId: null },
|
||||
list: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
chartData: [],
|
||||
dialog: { visible: false, title: '', form: {} },
|
||||
rules: {
|
||||
orderId: [{ required: true, message: '请输入订单ID', trigger: 'blur' }],
|
||||
itemId: [{ required: true, message: '请输入明细ID', trigger: 'blur' }],
|
||||
receivedQty: [{ required: true, message: '请输入收货数量', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadData()
|
||||
console.log('this.slots', this.$slots)
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.loading = true
|
||||
listPurchaseReceipt(this.query)
|
||||
.then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.buildChart()
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.query = { pageNum: 1, pageSize: 10, orderId: null, itemId: null }
|
||||
this.loadData()
|
||||
},
|
||||
openDialog(row) {
|
||||
if (row) {
|
||||
this.dialog.form = { ...row }
|
||||
this.dialog.title = '编辑收货'
|
||||
} else {
|
||||
this.dialog.form = { orderId: '', itemId: '', receivedQty: 0, qualityResult: 'OK' }
|
||||
this.dialog.title = '新增收货'
|
||||
}
|
||||
this.dialog.visible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
},
|
||||
submit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
const api = this.dialog.form.receiptId ? updatePurchaseReceipt : addPurchaseReceipt
|
||||
api(this.dialog.form).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.dialog.visible = false
|
||||
this.loadData()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$confirm('确定删除该收货记录吗?', '提示').then(() => {
|
||||
return delPurchaseReceipt(row.receiptId)
|
||||
}).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadData()
|
||||
})
|
||||
},
|
||||
buildChart() {
|
||||
const map = {}
|
||||
this.list.forEach(item => {
|
||||
const date = (item.receiptTime || '').slice(0, 10)
|
||||
if (!map[date]) {
|
||||
map[date] = { date, totalQty: 0, qualifiedQty: 0, ngQty: 0 }
|
||||
}
|
||||
map[date].totalQty += Number(item.receivedQty || 0)
|
||||
if (item.qualityResult === 'NG') {
|
||||
map[date].ngQty += Number(item.receivedQty || 0)
|
||||
} else {
|
||||
map[date].qualifiedQty += Number(item.receivedQty || 0)
|
||||
}
|
||||
})
|
||||
this.chartData = Object.values(map).sort((a, b) => a.date.localeCompare(b.date))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erp-receipt-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.surface-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
.surface-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.inline-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
> *:not(.filter-actions) {
|
||||
flex: 1 1 150px;
|
||||
}
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
.danger {
|
||||
color: #c0392b;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
<template>
|
||||
<div class="erp-report-page">
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>采购汇总</span>
|
||||
<el-date-picker
|
||||
v-model="range"
|
||||
type="datetimerange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
size="small"
|
||||
unlink-panels
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
@change="loadSummary"
|
||||
/>
|
||||
</header>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<p class="label">采购总金额</p>
|
||||
<p class="value">¥{{ summary.totalAmount | formatMoney }}</p>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<p class="label">供应商数量</p>
|
||||
<p class="value">{{ supplierBrief.length }}</p>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<p class="label">最大供应商金额</p>
|
||||
<p class="value">¥{{ topSupplierAmount | formatMoney }}</p>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<p class="label">平均订单金额</p>
|
||||
<p class="value">¥{{ avgSupplierAmount | formatMoney }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="surface-panel">
|
||||
<div class="report-grid">
|
||||
<div class="report-card">
|
||||
<h4>供应商汇总</h4>
|
||||
<el-table :data="supplierBrief" border size="small" height="320">
|
||||
<el-table-column prop="supplierId" label="供应商ID" />
|
||||
<el-table-column prop="orderCount" label="订单数" />
|
||||
<el-table-column prop="totalAmount" label="总金额" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="report-card">
|
||||
<h4>价格趋势</h4>
|
||||
<el-table :data="priceTrend" border size="small" height="320">
|
||||
<el-table-column prop="period" label="月份" />
|
||||
<el-table-column prop="materialCode" label="物料编码" />
|
||||
<el-table-column prop="avgPrice" label="平均含税价" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="report-card">
|
||||
<h4>供应商退货率</h4>
|
||||
<el-table :data="supplierQuality" border size="small" height="320">
|
||||
<el-table-column prop="supplierId" label="供应商ID" />
|
||||
<el-table-column prop="receivedQty" label="收货量" />
|
||||
<el-table-column prop="returnQty" label="退货量" />
|
||||
<el-table-column label="退货率">
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.returnRate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPurchaseReportSummary, getPurchasePriceTrend, getSupplierQuality } from '@/api/erp/purchase'
|
||||
|
||||
export default {
|
||||
name: 'ErpPurchaseReport',
|
||||
data() {
|
||||
return {
|
||||
range: [],
|
||||
summary: { totalAmount: 0 },
|
||||
supplierBrief: [],
|
||||
priceTrend: [],
|
||||
supplierQuality: []
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
topSupplierAmount() {
|
||||
if (!this.supplierBrief.length) return 0
|
||||
return Math.max(...this.supplierBrief.map(item => Number(item.totalAmount || 0)))
|
||||
},
|
||||
avgSupplierAmount() {
|
||||
if (!this.supplierBrief.length) return 0
|
||||
const sum = this.supplierBrief.reduce((acc, cur) => acc + Number(cur.totalAmount || 0), 0)
|
||||
return sum / this.supplierBrief.length
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadSummary()
|
||||
},
|
||||
methods: {
|
||||
buildParams() {
|
||||
const params = {}
|
||||
if (this.range && this.range.length === 2) {
|
||||
params.beginTime = this.range[0]
|
||||
params.endTime = this.range[1]
|
||||
}
|
||||
return params
|
||||
},
|
||||
loadSummary() {
|
||||
const params = this.buildParams()
|
||||
getPurchaseReportSummary(params).then(res => {
|
||||
this.summary = { totalAmount: res.totalAmount || 0 }
|
||||
this.supplierBrief = res.bySupplier || []
|
||||
})
|
||||
getPurchasePriceTrend(params).then(res => {
|
||||
this.priceTrend = res || []
|
||||
})
|
||||
getSupplierQuality(params).then(res => {
|
||||
this.supplierQuality = res || []
|
||||
})
|
||||
},
|
||||
formatPercent(value) {
|
||||
if (!value || !isFinite(value)) return '0%'
|
||||
return `${(Number(value) * 100).toFixed(2)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erp-report-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.surface-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
.surface-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.metric-card {
|
||||
border: 1px solid #d9dee4;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 4px;
|
||||
.label {
|
||||
color: #5b6875;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
}
|
||||
.report-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.report-card {
|
||||
border: 1px solid #d9dee4;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background: #fbfbfc;
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2b3440;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
<template>
|
||||
<div class="erp-requirement-page">
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>采购需求分析</span>
|
||||
<div class="surface-actions">
|
||||
<el-switch v-model="persistResult" active-text="写入建议表" inactive-text="仅计算" />
|
||||
<el-button type="primary" size="small" @click="handleAnalyze" :loading="loading">执行分析</el-button>
|
||||
</div>
|
||||
</header>
|
||||
<el-table :data="mappingRows" border size="small" class="mapping-table">
|
||||
<el-table-column label="产品">
|
||||
<template slot-scope="scope">
|
||||
<!-- <el-input v-model="scope.row.productId" placeholder="产品ID" /> -->
|
||||
<ProductSelect v-model="scope.row.productId" placeholder="选择产品" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="原料">
|
||||
<template slot-scope="scope">
|
||||
<!-- <el-input v-model="scope.row.rawMaterialId" placeholder="原料ID" /> -->
|
||||
<RawMaterialSelect v-model="scope.row.rawMaterialId" placeholder="选择原料" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转换率">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.conversionRate" :min="0" :max="1" :step="0.01" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" @click="removeMapping(scope.$index)" :disabled="mappingRows.length === 1">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-button icon="el-icon-plus" size="mini" class="add-btn" @click="addMapping">新增映射</el-button>
|
||||
|
||||
<el-table
|
||||
:data="resultList"
|
||||
border
|
||||
size="small"
|
||||
v-loading="loading"
|
||||
show-summary
|
||||
:summary-method="summaryMethod"
|
||||
>
|
||||
<el-table-column label="产品">
|
||||
<template slot-scope="scope">
|
||||
<div class="title">{{ scope.row.productName || '-' }}</div>
|
||||
<div class="cell-sub">ID: {{ scope.row.productId || '-' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="specification" label="规格" />
|
||||
<el-table-column prop="salesDemand" label="销售需求(吨)" />
|
||||
<el-table-column prop="productStockWeight" label="成品库存(吨)" />
|
||||
<el-table-column prop="rawStockConverted" label="原料折算(吨)" />
|
||||
<el-table-column prop="inTransitConverted" label="在途折算(吨)" />
|
||||
<el-table-column prop="pendingConverted" label="待下达折算(吨)" />
|
||||
<el-table-column prop="suggestedPurchase" label="建议采购(吨)">
|
||||
<template slot-scope="scope">
|
||||
<span :class="scope.row.suggestedPurchase > 0 ? 'highlight' : ''">
|
||||
{{ scope.row.suggestedPurchase }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>原料折算详情</span>
|
||||
</header>
|
||||
<el-table :data="rawDetailData" size="small" border height="320">
|
||||
<el-table-column prop="productName" label="产品" />
|
||||
<el-table-column prop="rawMaterialName" label="原料" />
|
||||
<el-table-column prop="conversionRate" label="转换率" />
|
||||
<el-table-column prop="stockWeight" label="库存重量" />
|
||||
<el-table-column prop="stockCoilCount" label="库存卷数" />
|
||||
<el-table-column prop="convertedStock" label="折算库存" />
|
||||
<el-table-column prop="convertedInTransit" label="折算在途" />
|
||||
<el-table-column prop="convertedPending" label="折算待下达" />
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { analyzePurchaseRequirement } from '@/api/erp/purchase'
|
||||
import RawMaterialSelect from '@/components/KLPService/RawMaterialSelect'
|
||||
import ProductSelect from '@/components/KLPService/ProductSelect'
|
||||
|
||||
export default {
|
||||
name: 'ErpPurchaseRequirement',
|
||||
components: {
|
||||
RawMaterialSelect,
|
||||
ProductSelect
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mappingRows: [{ productId: '', rawMaterialId: '', conversionRate: 1 }],
|
||||
persistResult: false,
|
||||
loading: false,
|
||||
resultList: [],
|
||||
rawDetailData: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addMapping() {
|
||||
this.mappingRows.push({ productId: '', rawMaterialId: '', conversionRate: 1 })
|
||||
},
|
||||
removeMapping(idx) {
|
||||
if (this.mappingRows.length === 1) return
|
||||
this.mappingRows.splice(idx, 1)
|
||||
},
|
||||
handleAnalyze() {
|
||||
if (!this.mappingRows.every(row => row.productId && row.rawMaterialId)) {
|
||||
this.$message.warning('请完善产品与原料映射')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
analyzePurchaseRequirement({
|
||||
persistResult: this.persistResult,
|
||||
mappings: this.mappingRows.map(row => ({
|
||||
productId: Number(row.productId),
|
||||
rawMaterialId: Number(row.rawMaterialId),
|
||||
conversionRate: Number(row.conversionRate || 0)
|
||||
}))
|
||||
})
|
||||
.then(res => {
|
||||
this.resultList = res || []
|
||||
this.buildRawDetail(res || [])
|
||||
this.$message.success('分析完成')
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
buildRawDetail(list) {
|
||||
const rows = []
|
||||
list.forEach(item => {
|
||||
(item.rawDetails || []).forEach(detail => {
|
||||
rows.push({
|
||||
productName: item.productName,
|
||||
rawMaterialName: detail.rawMaterialName,
|
||||
conversionRate: detail.conversionRate,
|
||||
stockWeight: detail.stockWeight,
|
||||
stockCoilCount: detail.stockCoilCount,
|
||||
convertedStock: detail.convertedStock,
|
||||
convertedInTransit: detail.convertedInTransit,
|
||||
convertedPending: detail.convertedPending
|
||||
})
|
||||
})
|
||||
})
|
||||
this.rawDetailData = rows
|
||||
},
|
||||
summaryMethod({ data }) {
|
||||
const sums = []
|
||||
const fields = ['salesDemand', 'productStockWeight', 'rawStockConverted', 'inTransitConverted', 'pendingConverted', 'suggestedPurchase']
|
||||
sums[0] = '总计'
|
||||
data.forEach(row => {
|
||||
fields.forEach((field, idx) => {
|
||||
sums[idx + 2] = (Number(sums[idx + 2]) || 0) + Number(row[field] || 0)
|
||||
})
|
||||
})
|
||||
return sums
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erp-requirement-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.surface-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
.surface-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.surface-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.mapping-table {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.add-btn {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.cell-sub {
|
||||
color: #7c8792;
|
||||
font-size: 12px;
|
||||
}
|
||||
.highlight {
|
||||
color: #c0392b;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
<template>
|
||||
<div class="erp-return-page">
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>退货单</span>
|
||||
<el-button type="primary" size="small" @click="openReturnDialog()">新增退货单</el-button>
|
||||
</header>
|
||||
<div class="inline-filter">
|
||||
<!-- <el-input v-model="returnQuery.orderId" placeholder="订单ID" size="small" clearable /> -->
|
||||
<OrderSelect v-model="returnQuery.orderId" placeholder="订单ID" size="small" clearable />
|
||||
<el-select v-model="returnQuery.status" placeholder="状态" size="small" clearable>
|
||||
<el-option label="草稿" :value="0" />
|
||||
<el-option label="完成" :value="1" />
|
||||
</el-select>
|
||||
<div class="filter-actions">
|
||||
<el-button size="small" type="primary" @click="loadReturns">查询</el-button>
|
||||
<el-button size="small" @click="resetReturnQuery">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="return-card-grid" v-loading="returnLoading">
|
||||
<div
|
||||
v-for="item in returnList"
|
||||
:key="item.returnId"
|
||||
class="return-card"
|
||||
:class="{ active: selectedReturnId === item.returnId }"
|
||||
@click="selectReturn(item)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="title">退货单 {{ item.returnId }}</p>
|
||||
<p class="sub">订单 {{ item.orderId }}</p>
|
||||
</div>
|
||||
<el-tag :type="item.status === 1 ? 'success' : 'info'" size="mini">
|
||||
{{ item.status === 1 ? '完成' : '草稿' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><span>类型</span>{{ item.returnType || '-' }}</p>
|
||||
<p><span>原因</span>{{ item.reason || '-' }}</p>
|
||||
<p><span>退货日期</span>{{ item.returnDate || '-' }}</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button type="text" size="mini" @click.stop="openReturnDialog(item)">编辑</el-button>
|
||||
<el-button type="text" size="mini" @click.stop="openReturnItems(item)">明细</el-button>
|
||||
<el-button type="text" size="mini" class="danger" @click.stop="handleDeleteReturn(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!returnList.length && !returnLoading" description="暂无退货单" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="surface-panel">
|
||||
<header class="surface-header">
|
||||
<span>退货明细</span>
|
||||
<el-button type="primary" size="small" @click="openReturnItemDialog()">新增明细</el-button>
|
||||
</header>
|
||||
<div class="inline-filter">
|
||||
<!-- <el-input v-model="returnItemQuery.returnId" placeholder="退货单ID" size="small" clearable /> -->
|
||||
<OrderSelect v-model="returnItemQuery.orderId" />
|
||||
<OrderDetailSelect v-model="returnItemQuery.itemId" :orderId="returnItemQuery.orderId" />
|
||||
<div class="filter-actions">
|
||||
<el-button size="small" type="primary" @click="loadReturnItems">查询</el-button>
|
||||
<el-button size="small" @click="resetReturnItemQuery">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="returnItemList" border size="small" v-loading="returnItemLoading">
|
||||
<!-- <el-table-column prop="returnId" label="退货单ID" /> -->
|
||||
<el-table-column prop="itemId" label="订单明细编号" />
|
||||
<el-table-column prop="returnQty" label="数量" />
|
||||
<el-table-column label="问题照片">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip v-if="scope.row.photos" :content="scope.row.photos" placement="top">
|
||||
<span class="link">查看</span>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="openReturnItemDialog(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" class="danger" @click="handleDeleteReturnItem(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<el-dialog :title="returnDialog.title" :visible.sync="returnDialog.visible" width="520px">
|
||||
<el-form :model="returnDialog.form" :rules="returnRules" ref="returnForm" label-width="100px" size="small">
|
||||
<el-form-item label="订单ID" prop="orderId">
|
||||
<!-- <el-input v-model="returnDialog.form.orderId" /> -->
|
||||
<OrderSelect v-model="returnDialog.form.orderId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="退货类型">
|
||||
<el-select v-model="returnDialog.form.returnType">
|
||||
<el-option label="质量问题" value="QUALITY" />
|
||||
<el-option label="数量错误" value="QTY" />
|
||||
<el-option label="规格不符" value="SPEC" />
|
||||
<el-option label="协商退货" value="OTHER" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="退货原因">
|
||||
<el-input type="textarea" v-model="returnDialog.form.reason" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="returnDialog.form.status">
|
||||
<el-option label="草稿" :value="0" />
|
||||
<el-option label="完成" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input type="textarea" v-model="returnDialog.form.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="returnDialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitReturn">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="returnItemDialog.title" :visible.sync="returnItemDialog.visible" width="500px">
|
||||
<el-form :model="returnItemDialog.form" :rules="returnItemRules" ref="returnItemForm" label-width="120px" size="small">
|
||||
<!-- <el-form-item label="退货单ID" prop="returnId">
|
||||
<el-input v-model="returnItemDialog.form.returnId" />
|
||||
</el-form-item> -->
|
||||
<!-- 先选择订单id,再选择订单明细id, -->
|
||||
<el-form-item label="选择订单" prop="orderId">
|
||||
<!-- <el-input v-model="returnItemDialog.form.orderId" /> -->
|
||||
<OrderSelect v-model="returnItemDialog.form.orderId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单明细编号" prop="itemId">
|
||||
<!-- <el-input v-model="returnItemDialog.form.itemId" /> -->
|
||||
<OrderDetailSelect v-model="returnItemDialog.form.itemId" :orderId="returnItemDialog.form.orderId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="退货数量" prop="returnQty">
|
||||
<el-input-number size="mini" v-model="returnItemDialog.form.returnQty" :min="0" :precision="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="问题照片">
|
||||
<el-input v-model="returnItemDialog.form.photos" placeholder="多张以逗号分隔" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input type="textarea" v-model="returnItemDialog.form.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="returnItemDialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitReturnItem">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listPurchaseReturn,
|
||||
addPurchaseReturn,
|
||||
updatePurchaseReturn,
|
||||
delPurchaseReturn,
|
||||
listPurchaseReturnItem,
|
||||
addPurchaseReturnItem,
|
||||
updatePurchaseReturnItem,
|
||||
delPurchaseReturnItem
|
||||
} from '@/api/erp/purchase'
|
||||
import OrderSelect from '@/components/KLPService/OrderSelect'
|
||||
import OrderDetailSelect from '@/components/KLPService/OrderDetailSelect'
|
||||
|
||||
export default {
|
||||
name: 'ErpPurchaseReturn',
|
||||
components: {
|
||||
OrderSelect,
|
||||
OrderDetailSelect
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
returnQuery: { pageNum: 1, pageSize: 10, orderId: null, status: null },
|
||||
returnList: [],
|
||||
returnLoading: false,
|
||||
selectedReturnId: null,
|
||||
returnDialog: { visible: false, title: '', form: {} },
|
||||
returnRules: { orderId: [{ required: true, message: '请输入订单ID', trigger: 'blur' }] },
|
||||
returnItemQuery: { pageNum: 1, pageSize: 10, returnId: null, itemId: null },
|
||||
returnItemList: [],
|
||||
returnItemLoading: false,
|
||||
returnItemDialog: { visible: false, title: '', form: {
|
||||
orderId: '',
|
||||
} },
|
||||
returnItemRules: {
|
||||
returnId: [{ required: true, message: '请输入退货单ID', trigger: 'blur' }],
|
||||
itemId: [{ required: true, message: '请输入订单明细ID', trigger: 'blur' }],
|
||||
returnQty: [{ required: true, message: '请输入退货数量', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadReturns()
|
||||
this.loadReturnItems()
|
||||
},
|
||||
methods: {
|
||||
loadReturns() {
|
||||
this.returnLoading = true
|
||||
listPurchaseReturn(this.returnQuery)
|
||||
.then(res => {
|
||||
this.returnList = res.rows || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.returnLoading = false
|
||||
})
|
||||
},
|
||||
resetReturnQuery() {
|
||||
this.returnQuery = { pageNum: 1, pageSize: 10, orderId: null, status: null }
|
||||
this.loadReturns()
|
||||
},
|
||||
openReturnDialog(row) {
|
||||
if (row) {
|
||||
this.returnDialog.form = { ...row }
|
||||
this.returnDialog.title = '编辑退货单'
|
||||
} else {
|
||||
this.returnDialog.form = { orderId: '', returnType: 'QUALITY', status: 0 }
|
||||
this.returnDialog.title = '新增退货单'
|
||||
}
|
||||
this.returnDialog.visible = true
|
||||
this.$nextTick(() => this.$refs.returnForm && this.$refs.returnForm.clearValidate())
|
||||
},
|
||||
submitReturn() {
|
||||
this.$refs.returnForm.validate(valid => {
|
||||
if (!valid) return
|
||||
const api = this.returnDialog.form.returnId ? updatePurchaseReturn : addPurchaseReturn
|
||||
api(this.returnDialog.form).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.returnDialog.visible = false
|
||||
this.loadReturns()
|
||||
})
|
||||
})
|
||||
},
|
||||
selectReturn(row) {
|
||||
this.selectedReturnId = row.returnId;
|
||||
this.returnItemDialog.form.orderId = row.orderId
|
||||
this.returnItemQuery.orderId = row.orderId
|
||||
this.openReturnItems(row)
|
||||
},
|
||||
openReturnItems(row) {
|
||||
this.selectedReturnId = row.returnId
|
||||
this.returnItemQuery.returnId = row.returnId
|
||||
this.loadReturnItems()
|
||||
},
|
||||
handleDeleteReturn(row) {
|
||||
this.$confirm('确定删除该退货单吗?', '提示').then(() => delPurchaseReturn(row.returnId)).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadReturns()
|
||||
})
|
||||
},
|
||||
loadReturnItems() {
|
||||
this.returnItemLoading = true
|
||||
listPurchaseReturnItem(this.returnItemQuery)
|
||||
.then(res => {
|
||||
this.returnItemList = res.rows || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.returnItemLoading = false
|
||||
})
|
||||
},
|
||||
resetReturnItemQuery() {
|
||||
this.returnItemQuery = { pageNum: 1, pageSize: 10, returnId: null, itemId: null }
|
||||
this.loadReturnItems()
|
||||
},
|
||||
openReturnItemDialog(row) {
|
||||
if (row) {
|
||||
this.returnItemDialog.form = { ...row, orderId: this.returnItemDialog.form.orderId }
|
||||
this.returnItemDialog.title = '编辑退货明细'
|
||||
} else {
|
||||
this.returnItemDialog.form = { returnId: this.returnItemQuery.returnId || '', itemId: '', returnQty: 0, orderId: this.returnItemDialog.form.orderId }
|
||||
this.returnItemDialog.title = '新增退货明细'
|
||||
}
|
||||
this.returnItemDialog.visible = true
|
||||
this.$nextTick(() => this.$refs.returnItemForm && this.$refs.returnItemForm.clearValidate())
|
||||
},
|
||||
submitReturnItem() {
|
||||
this.$refs.returnItemForm.validate(valid => {
|
||||
if (!valid) return
|
||||
const api = this.returnItemDialog.form.returnItemId ? updatePurchaseReturnItem : addPurchaseReturnItem
|
||||
api(this.returnItemDialog.form).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.returnItemDialog.visible = false
|
||||
this.loadReturnItems()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDeleteReturnItem(row) {
|
||||
this.$confirm('确定删除该明细吗?', '提示').then(() => delPurchaseReturnItem(row.returnItemId)).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadReturnItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erp-return-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.surface-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
.surface-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #1e2c39;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.inline-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
> *:not(.filter-actions) {
|
||||
flex: 1 1 160px;
|
||||
}
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
.return-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.return-card {
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background: #fbfbfc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
&.active {
|
||||
border-color: #409eff;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 6px 20px rgba(16, 38, 70, 0.08);
|
||||
}
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
.title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #15202b;
|
||||
}
|
||||
.sub {
|
||||
margin: 2px 0 0;
|
||||
color: #7a8699;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.card-body {
|
||||
p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #4a5663;
|
||||
margin: 0;
|
||||
span {
|
||||
color: #98a1ac;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
.danger {
|
||||
color: #c0392b;
|
||||
}
|
||||
}
|
||||
.link {
|
||||
color: #2f86d7;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,545 +1,232 @@
|
||||
<template>
|
||||
<div class="erp-supplier-page">
|
||||
<section class="surface-panel supplier-panel">
|
||||
<header class="surface-header">
|
||||
<span>供应商档案</span>
|
||||
<el-button type="primary" size="small" @click="openSupplierDialog()">新增</el-button>
|
||||
</header>
|
||||
<div class="inline-filter">
|
||||
<el-input v-model="supplierQuery.name" placeholder="供应商名称" size="small" clearable />
|
||||
<el-select v-model="supplierQuery.creditRating" placeholder="信用等级" size="small" clearable>
|
||||
<el-option label="A" value="A" />
|
||||
<el-option label="B" value="B" />
|
||||
<el-option label="C" value="C" />
|
||||
<el-option label="D" value="D" />
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" size="small">
|
||||
<el-form-item label="编码" prop="supplierCode">
|
||||
<el-input v-model="queryParams.supplierCode" clearable placeholder="供应商编码" style="width:150px" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="queryParams.name" clearable placeholder="供应商名称" style="width:170px" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="queryParams.type" clearable placeholder="全部" style="width:140px">
|
||||
<el-option v-for="t in typeOptions" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
<div class="filter-actions">
|
||||
<el-button size="small" type="primary" @click="loadSuppliers">查询</el-button>
|
||||
<el-button size="small" @click="resetSupplierQuery">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="supplier-grid" v-loading="supplierLoading">
|
||||
<div
|
||||
v-for="item in supplierList"
|
||||
:key="item.supplierId"
|
||||
class="supplier-card"
|
||||
>
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<p class="title">{{ item.name }}</p>
|
||||
<p class="sub">编码:{{ item.supplierCode || '-' }}</p>
|
||||
</div>
|
||||
<div class="tag-group">
|
||||
<el-tag size="mini" type="info">{{ supplierTypeLabel(item.type) }}</el-tag>
|
||||
<el-tag size="mini" type="plain">信用 {{ item.creditRating || '-' }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="card-body">
|
||||
<li><span>联系人</span><strong>{{ item.contactPerson || '-' }}</strong></li>
|
||||
<li><span>电话</span><strong>{{ item.contactPhone || '-' }}</strong></li>
|
||||
<li><span>地址</span><strong>{{ item.address || '-' }}</strong></li>
|
||||
<li><span>备注</span><strong>{{ item.remark || '-' }}</strong></li>
|
||||
</ul>
|
||||
<div class="card-actions">
|
||||
<el-button type="text" size="mini" @click="openSupplierDialog(item)">编辑</el-button>
|
||||
<el-button type="text" size="mini" @click="openPriceDrawer(item)">价格</el-button>
|
||||
<el-button type="text" size="mini" class="danger" @click="handleDeleteSupplier(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!supplierList.length && !supplierLoading" description="暂无数据" />
|
||||
</div>
|
||||
</section>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-drawer
|
||||
title="供应商价格表"
|
||||
:visible.sync="priceDrawer.visible"
|
||||
size="480px"
|
||||
direction="rtl"
|
||||
append-to-body
|
||||
custom-class="price-drawer"
|
||||
@close="closePriceDrawer"
|
||||
>
|
||||
<div class="price-drawer-body">
|
||||
<div class="drawer-header" v-if="priceDrawer.supplier">
|
||||
<div class="drawer-title-block">
|
||||
<p class="drawer-title">{{ priceDrawer.supplier.name }}</p>
|
||||
<p class="drawer-sub">编码:{{ priceDrawer.supplier.supplierCode || '-' }}</p>
|
||||
</div>
|
||||
<el-button type="primary" size="mini" @click="openPriceDialog()">新增价格</el-button>
|
||||
</div>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['erp:supplier:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete()" v-hasPermi="['erp:supplier:remove']">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-title">价格列表</div>
|
||||
<el-collapse v-model="activePricePanels" accordion>
|
||||
<el-collapse-item
|
||||
v-for="item in priceList"
|
||||
:name="item.priceId"
|
||||
:title="`${item.materialTypeCode || '未指定'} · ¥${item.price || 0}`"
|
||||
:key="item.priceId"
|
||||
>
|
||||
<ul class="price-info">
|
||||
<li><span>规格</span>{{ item.specification || '-' }}</li>
|
||||
<li><span>含税价</span>{{ item.price || '-' }}</li>
|
||||
<li><span>有效期</span>{{ formatRange(item.validFrom, item.validTo) }}</li>
|
||||
<li><span>备注</span>{{ item.remark || '-' }}</li>
|
||||
</ul>
|
||||
<div class="price-actions">
|
||||
<el-button type="text" size="mini" @click="openPriceDialog(item)">编辑</el-button>
|
||||
<el-button type="text" size="mini" class="danger" @click="handleDeletePrice(item)">删除</el-button>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty v-if="!priceList.length && !priceLoading" description="暂无价格记录" />
|
||||
<el-skeleton v-if="priceLoading" animated rows="4" />
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<el-table v-loading="loading" :data="list" border size="small" @selection-change="onSelectionChange">
|
||||
<el-table-column type="selection" width="46" align="center" />
|
||||
<el-table-column label="供应商编码" prop="supplierCode" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="供应商名称" prop="name" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="120" align="center">
|
||||
<template slot-scope="s">{{ typeText(s.row.type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="信用等级" width="90" align="center">
|
||||
<template slot-scope="s"><span class="sp-credit" :class="'c' + (s.row.creditRating || '')">{{ s.row.creditRating || '—' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系人" prop="contactPerson" width="100" />
|
||||
<el-table-column label="联系电话" prop="contactPhone" width="130" />
|
||||
<el-table-column label="地址" prop="address" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="130" align="center" fixed="right">
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleUpdate(s.row)" v-hasPermi="['erp:supplier:edit']">编辑</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(s.row)" v-hasPermi="['erp:supplier:remove']">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 供应商弹窗 -->
|
||||
<el-dialog :title="supplierDialog.title" :visible.sync="supplierDialog.visible" width="520px">
|
||||
<el-form :model="supplierDialog.form" :rules="supplierRules" ref="supplierForm" label-width="90px" size="small">
|
||||
<el-form-item label="编码" prop="supplierCode">
|
||||
<el-input v-model="supplierDialog.form.supplierCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="supplierDialog.form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="supplierDialog.form.type" placeholder="请选择类型">
|
||||
<el-option v-for="opt in supplierTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="信用等级">
|
||||
<el-select v-model="supplierDialog.form.creditRating" clearable>
|
||||
<el-option label="A" value="A" />
|
||||
<el-option label="B" value="B" />
|
||||
<el-option label="C" value="C" />
|
||||
<el-option label="D" value="D" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="supplierDialog.form.contactPerson" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="supplierDialog.form.contactPhone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="地址">
|
||||
<el-input v-model="supplierDialog.form.address" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input type="textarea" v-model="supplierDialog.form.remark" />
|
||||
</el-form-item>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="open" width="640px" append-to-body>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="90px" size="small">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="供应商编码" prop="supplierCode">
|
||||
<el-input v-model="form.supplierCode" placeholder="请输入编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="供应商名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="form.type" clearable placeholder="请选择" style="width:100%">
|
||||
<el-option v-for="t in typeOptions" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="信用等级" prop="creditRating">
|
||||
<el-select v-model="form.creditRating" clearable placeholder="请选择" style="width:100%">
|
||||
<el-option v-for="c in creditOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系人" prop="contactPerson">
|
||||
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" prop="contactPhone">
|
||||
<el-input v-model="form.contactPhone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="supplierDialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitSupplier">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 价格弹窗 -->
|
||||
<el-dialog :title="priceDialog.title" :visible.sync="priceDialog.visible" width="520px">
|
||||
<el-form :model="priceDialog.form" :rules="priceRules" ref="priceForm" label-width="100px" size="small">
|
||||
<el-form-item label="供应商" prop="supplierId">
|
||||
<el-select v-model="priceDialog.form.supplierId" placeholder="选择供应商">
|
||||
<el-option v-for="sp in supplierOptions" :key="sp.supplierId" :label="sp.name" :value="sp.supplierId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="原料" prop="rawMaterialId">
|
||||
<RawMaterialSelect v-model="priceDialog.form.rawMaterialId" @change="handleMaterialChange" />
|
||||
</el-form-item>
|
||||
<el-form-item label="物料编码">
|
||||
<el-input v-model="priceDialog.form.materialTypeCode" placeholder="选择原料后自动带出" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格">
|
||||
<el-input v-model="priceDialog.form.specification" placeholder="选择原料后可调整" />
|
||||
</el-form-item>
|
||||
<el-form-item label="含税价格" prop="price">
|
||||
<el-input-number v-model="priceDialog.form.price" :min="0" :precision="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="有效期">
|
||||
<el-date-picker
|
||||
v-model="priceValidRange"
|
||||
type="daterange"
|
||||
value-format="yyyy-MM-dd"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input type="textarea" v-model="priceDialog.form.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="priceDialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitPrice">保 存</el-button>
|
||||
<el-button @click="open = false">取消</el-button>
|
||||
<el-button type="primary" :loading="buttonLoading" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listSupplier,
|
||||
addSupplier,
|
||||
updateSupplier,
|
||||
delSupplier,
|
||||
listSupplierPrice,
|
||||
addSupplierPrice,
|
||||
updateSupplierPrice,
|
||||
delSupplierPrice
|
||||
} from '@/api/erp/purchase'
|
||||
import RawMaterialSelect from '@/components/KLPService/RawMaterialSelect'
|
||||
import { listSupplier, addSupplier, updateSupplier, delSupplier } from '@/api/erp/purchase'
|
||||
|
||||
export default {
|
||||
name: 'ErpSupplierManage',
|
||||
components: { RawMaterialSelect },
|
||||
name: 'ErpSupplier',
|
||||
data() {
|
||||
return {
|
||||
supplierTypeOptions: [
|
||||
{ label: '原料供应商', value: 'RAW' },
|
||||
{ label: '其他供应商', value: 'OTHER' }
|
||||
loading: true,
|
||||
buttonLoading: false,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
list: [],
|
||||
ids: [],
|
||||
multiple: true,
|
||||
title: '',
|
||||
open: false,
|
||||
form: {},
|
||||
queryParams: { pageNum: 1, pageSize: 20, supplierCode: undefined, name: undefined, type: undefined },
|
||||
typeOptions: [
|
||||
{ value: 'RAW', label: '原料供应商' },
|
||||
{ value: 'AUX', label: '辅料供应商' },
|
||||
{ value: 'OTHER', label: '其他' }
|
||||
],
|
||||
supplierQuery: { pageNum: 1, pageSize: 50, name: null, creditRating: null },
|
||||
supplierList: [],
|
||||
supplierLoading: false,
|
||||
supplierDialog: { visible: false, title: '', form: {} },
|
||||
supplierRules: {
|
||||
supplierCode: [{ required: true, message: '请输入编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
},
|
||||
priceQuery: { pageNum: 1, pageSize: 50, supplierId: null },
|
||||
priceList: [],
|
||||
priceLoading: false,
|
||||
priceDialog: { visible: false, title: '', form: {} },
|
||||
priceRules: {
|
||||
supplierId: [{ required: true, message: '请选择供应商', trigger: 'change' }],
|
||||
rawMaterialId: [{ required: true, message: '请选择原料', trigger: 'change' }],
|
||||
price: [{ required: true, message: '请输入价格', trigger: 'change' }]
|
||||
},
|
||||
priceValidRange: [],
|
||||
supplierOptions: [],
|
||||
priceDrawer: { visible: false, supplier: null },
|
||||
activePricePanels: []
|
||||
creditOptions: ['A', 'B', 'C', 'D'],
|
||||
rules: {
|
||||
supplierCode: [{ required: true, message: '请输入供应商编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadSuppliers()
|
||||
this.loadPrices()
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
loadSuppliers() {
|
||||
this.supplierLoading = true
|
||||
listSupplier(this.supplierQuery)
|
||||
.then(res => {
|
||||
this.supplierList = res.rows || []
|
||||
this.supplierOptions = this.supplierList
|
||||
})
|
||||
.finally(() => {
|
||||
this.supplierLoading = false
|
||||
})
|
||||
getList() {
|
||||
this.loading = true
|
||||
listSupplier(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
resetSupplierQuery() {
|
||||
this.supplierQuery = { pageNum: 1, pageSize: 50, name: null, creditRating: null }
|
||||
this.loadSuppliers()
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
openSupplierDialog(row) {
|
||||
if (row) {
|
||||
this.supplierDialog.form = { ...row }
|
||||
this.supplierDialog.title = '编辑供应商'
|
||||
} else {
|
||||
this.supplierDialog.form = { supplierCode: '', name: '', creditRating: 'A', type: 'RAW' }
|
||||
this.supplierDialog.title = '新增供应商'
|
||||
}
|
||||
this.supplierDialog.visible = true
|
||||
this.$nextTick(() => this.$refs.supplierForm && this.$refs.supplierForm.clearValidate())
|
||||
resetQuery() {
|
||||
this.resetForm('queryForm')
|
||||
this.handleQuery()
|
||||
},
|
||||
submitSupplier() {
|
||||
this.$refs.supplierForm.validate(valid => {
|
||||
onSelectionChange(sel) {
|
||||
this.ids = sel.map(i => i.supplierId)
|
||||
this.multiple = !sel.length
|
||||
},
|
||||
reset() {
|
||||
this.form = { supplierId: null, supplierCode: '', name: '', type: undefined, creditRating: undefined, contactPerson: '', contactPhone: '', address: '', remark: '' }
|
||||
this.resetForm('form')
|
||||
},
|
||||
handleAdd() {
|
||||
this.reset()
|
||||
this.title = '新增供应商'
|
||||
this.open = true
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
this.form = { ...row }
|
||||
this.title = '编辑供应商'
|
||||
this.open = true
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs['form'].validate(valid => {
|
||||
if (!valid) return
|
||||
const api = this.supplierDialog.form.supplierId ? updateSupplier : addSupplier
|
||||
api(this.supplierDialog.form).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.supplierDialog.visible = false
|
||||
this.loadSuppliers()
|
||||
})
|
||||
this.buttonLoading = true
|
||||
const api = this.form.supplierId ? updateSupplier : addSupplier
|
||||
api(this.form).then(() => {
|
||||
this.$modal.msgSuccess('保存成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
}).finally(() => { this.buttonLoading = false })
|
||||
})
|
||||
},
|
||||
handleDeleteSupplier(row) {
|
||||
this.$confirm('确认删除该供应商吗?', '提示').then(() => {
|
||||
return delSupplier(row.supplierId)
|
||||
handleDelete(row) {
|
||||
const ids = row && row.supplierId ? row.supplierId : this.ids
|
||||
const tip = row && row.name ? '「' + row.name + '」' : '选中的 ' + this.ids.length + ' 个供应商'
|
||||
this.$modal.confirm('确认删除' + tip + '?').then(() => {
|
||||
return delSupplier(ids)
|
||||
}).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadSuppliers()
|
||||
})
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
supplierTypeLabel(val) {
|
||||
const target = this.supplierTypeOptions.find(opt => opt.value === val)
|
||||
return target ? target.label : '其他供应商'
|
||||
handleExport() {
|
||||
this.download('erp/supplier/export', { ...this.queryParams }, `supplier_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
// price
|
||||
openPriceDrawer(item) {
|
||||
this.priceDrawer = { visible: true, supplier: item }
|
||||
this.priceQuery = { ...this.priceQuery, supplierId: item.supplierId, pageNum: 1 }
|
||||
this.loadPrices()
|
||||
},
|
||||
closePriceDrawer() {
|
||||
this.priceDrawer.visible = false
|
||||
this.priceDrawer.supplier = null
|
||||
this.priceList = []
|
||||
},
|
||||
loadPrices() {
|
||||
if (!this.priceQuery.supplierId) {
|
||||
this.priceList = []
|
||||
return
|
||||
}
|
||||
this.priceLoading = true
|
||||
listSupplierPrice(this.priceQuery)
|
||||
.then(res => {
|
||||
this.priceList = res.rows || []
|
||||
this.activePricePanels = (this.priceList[0] && [this.priceList[0].priceId]) || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.priceLoading = false
|
||||
})
|
||||
},
|
||||
matchSupplier(id) {
|
||||
const target = this.supplierOptions.find(sp => sp.supplierId === id)
|
||||
return target ? target.name : id
|
||||
},
|
||||
openPriceDialog(row) {
|
||||
if (!this.supplierOptions.length) {
|
||||
this.$message.warning('请先维护供应商')
|
||||
return
|
||||
}
|
||||
if (row) {
|
||||
this.priceDialog.form = { ...row }
|
||||
this.priceValidRange = [row.validFrom, row.validTo]
|
||||
this.priceDialog.title = '编辑价格'
|
||||
} else {
|
||||
const currentSupplierId = this.priceDrawer.supplier?.supplierId || null
|
||||
this.priceDialog.form = { supplierId: currentSupplierId, rawMaterialId: '', materialTypeCode: '', specification: '', price: 0 }
|
||||
this.priceValidRange = []
|
||||
this.priceDialog.title = '新增价格'
|
||||
}
|
||||
this.priceDialog.visible = true
|
||||
this.$nextTick(() => this.$refs.priceForm && this.$refs.priceForm.clearValidate())
|
||||
},
|
||||
submitPrice() {
|
||||
this.$refs.priceForm.validate(valid => {
|
||||
if (!valid) return
|
||||
if (this.priceValidRange && this.priceValidRange.length === 2) {
|
||||
this.priceDialog.form.validFrom = this.priceValidRange[0]
|
||||
this.priceDialog.form.validTo = this.priceValidRange[1]
|
||||
}
|
||||
if (!this.priceDialog.form.materialTypeCode) {
|
||||
this.$message.warning('请先选择原料')
|
||||
return
|
||||
}
|
||||
const api = this.priceDialog.form.priceId ? updateSupplierPrice : addSupplierPrice
|
||||
api(this.priceDialog.form).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.priceDialog.visible = false
|
||||
this.loadPrices()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDeletePrice(row) {
|
||||
this.$confirm('确认删除该价格吗?', '提示').then(() => {
|
||||
return delSupplierPrice(row.priceId)
|
||||
}).then(() => {
|
||||
this.$message.success('删除成功')
|
||||
this.loadPrices()
|
||||
})
|
||||
},
|
||||
handleMaterialChange(val, rows) {
|
||||
const target = Array.isArray(rows) ? rows[0] : null
|
||||
this.priceDialog.form.rawMaterialId = val
|
||||
if (target) {
|
||||
this.priceDialog.form.materialTypeCode = target.rawMaterialCode
|
||||
this.priceDialog.form.specification = target.specification
|
||||
}
|
||||
},
|
||||
formatRange(start, end) {
|
||||
if (!start && !end) return '-'
|
||||
return `${start || '-'} ~ ${end || '-'}`
|
||||
typeText(t) {
|
||||
const o = this.typeOptions.find(x => x.value === t)
|
||||
return o ? o.label : (t || '—')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erp-supplier-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.surface-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d6dce1;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.surface-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
color: #1b2a38;
|
||||
}
|
||||
.inline-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
> *:not(.filter-actions) {
|
||||
flex: 1 1 150px;
|
||||
}
|
||||
}
|
||||
.supplier-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.supplier-card {
|
||||
border: 1px solid #d6dce1;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #15212d;
|
||||
}
|
||||
.sub {
|
||||
margin: 2px 0 0;
|
||||
color: #79828c;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tag-group {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.card-body {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #4a5663;
|
||||
& + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
span {
|
||||
color: #8a96a3;
|
||||
}
|
||||
strong {
|
||||
color: #151e26;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
.danger {
|
||||
color: #c0392b;
|
||||
}
|
||||
}
|
||||
.price-drawer {
|
||||
.price-drawer-body {
|
||||
padding: 16px 20px 12px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.drawer-header {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.drawer-title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.drawer-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #1f2a37;
|
||||
}
|
||||
.drawer-sub {
|
||||
margin-top: 4px;
|
||||
color: #7a8694;
|
||||
font-size: 12px;
|
||||
}
|
||||
.drawer-section {
|
||||
margin-top: 14px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.drawer-section-title {
|
||||
font-size: 13px;
|
||||
color: #5c6c7b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
::v-deep .el-collapse-item__header {
|
||||
padding-left: 10px;
|
||||
}
|
||||
::v-deep .el-collapse-item__content {
|
||||
padding: 10px 10px 12px;
|
||||
}
|
||||
.price-info {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 4px;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #4a5663;
|
||||
& + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
span {
|
||||
color: #8a96a3;
|
||||
}
|
||||
}
|
||||
}
|
||||
.price-actions {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
.danger {
|
||||
color: #c0392b;
|
||||
}
|
||||
}
|
||||
$accent: #5b8db8;
|
||||
.sp-credit {
|
||||
display: inline-block; min-width: 18px; font-size: 12px; font-weight: 600; color: #909399;
|
||||
&.cA { color: #3a8a4d; }
|
||||
&.cB { color: $accent; }
|
||||
&.cC { color: #d6a256; }
|
||||
&.cD { color: #c45656; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user