ソースを参照

feat:生产计划

SJ 1 週間 前
コミット
a41614b88d

+ 253 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/controller/ProdPlanController.java

@@ -0,0 +1,253 @@
+package org.jeecg.modules.ProdPlan.controller;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.jeecg.common.api.vo.Result;
+import org.jeecg.common.system.query.QueryGenerator;
+import org.jeecg.common.util.oConvertUtils;
+import org.jeecg.modules.DefectiveProduct.service.IDefectiveProductService;
+import org.jeecg.modules.ProdPlan.entity.DelayProduct;
+import org.jeecg.modules.ProdPlan.entity.ProdPlan;
+import org.jeecg.modules.ProdPlan.service.IDelayProductService;
+import org.jeecg.modules.ProdPlan.service.IProdPlanService;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+
+import org.jeecgframework.poi.excel.ExcelImportUtil;
+import org.jeecgframework.poi.excel.def.NormalExcelConstants;
+import org.jeecgframework.poi.excel.entity.ExportParams;
+import org.jeecgframework.poi.excel.entity.ImportParams;
+import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
+import org.jeecg.common.system.base.controller.JeecgController;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartHttpServletRequest;
+import org.springframework.web.servlet.ModelAndView;
+import com.alibaba.fastjson.JSON;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.jeecg.common.aspect.annotation.AutoLog;
+import org.springframework.web.servlet.view.AbstractView;
+
+/**
+ * @Description: 生产计划
+ * @Author: jeecg-boot
+ * @Date:   2025-06-04
+ * @Version: V1.0
+ */
+@Api(tags="生产计划")
+@RestController
+@RequestMapping("/ProdPlan/prodPlan")
+@Slf4j
+public class ProdPlanController extends JeecgController<ProdPlan, IProdPlanService> {
+	@Autowired
+	private IProdPlanService prodPlanService;
+
+	 @Value(value = "${jeecg.path.upload}")
+	 private String uploadpath;
+
+	 @Autowired
+	 private IDelayProductService delayProductService;
+
+
+	/**
+	 * 分页列表查询
+	 *
+	 * @param prodPlan
+	 * @param pageNo
+	 * @param pageSize
+	 * @param req
+	 * @return
+	 */
+	//@AutoLog(value = "生产计划-分页列表查询")
+	@ApiOperation(value="生产计划-分页列表查询", notes="生产计划-分页列表查询")
+	@GetMapping(value = "/list")
+	public Result<IPage<ProdPlan>> queryPageList(ProdPlan prodPlan,
+								   @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
+								   @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
+								   HttpServletRequest req) {
+		QueryWrapper<ProdPlan> queryWrapper = QueryGenerator.initQueryWrapper(prodPlan, req.getParameterMap());
+		Page<ProdPlan> page = new Page<ProdPlan>(pageNo, pageSize);
+		IPage<ProdPlan> pageList = prodPlanService.page(page, queryWrapper);
+		return Result.OK(pageList);
+	}
+	
+	/**
+	 *   添加
+	 *
+	 * @param prodPlan
+	 * @return
+	 */
+	@AutoLog(value = "生产计划-添加")
+	@ApiOperation(value="生产计划-添加", notes="生产计划-添加")
+	@PostMapping(value = "/add")
+	public Result<String> add(@RequestBody ProdPlan prodPlan) {
+		prodPlanService.save(prodPlan);
+		return Result.OK("添加成功!");
+	}
+	
+	/**
+	 *  编辑
+	 *
+	 * @param prodPlan
+	 * @return
+	 */
+	@AutoLog(value = "生产计划-编辑")
+	@ApiOperation(value="生产计划-编辑", notes="生产计划-编辑")
+	@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
+	public Result<String> edit(@RequestBody ProdPlan prodPlan) {
+		prodPlanService.updateById(prodPlan);
+		return Result.OK("编辑成功!");
+	}
+	
+	/**
+	 *   通过id删除
+	 *
+	 * @param id
+	 * @return
+	 */
+	@AutoLog(value = "生产计划-通过id删除")
+	@ApiOperation(value="生产计划-通过id删除", notes="生产计划-通过id删除")
+	@DeleteMapping(value = "/delete")
+	public Result<String> delete(@RequestParam(name="id",required=true) String id) {
+		prodPlanService.removeById(id);
+		return Result.OK("删除成功!");
+	}
+	
+	/**
+	 *  批量删除
+	 *
+	 * @param ids
+	 * @return
+	 */
+	@AutoLog(value = "生产计划-批量删除")
+	@ApiOperation(value="生产计划-批量删除", notes="生产计划-批量删除")
+	@DeleteMapping(value = "/deleteBatch")
+	public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
+		this.prodPlanService.removeByIds(Arrays.asList(ids.split(",")));
+		return Result.OK("批量删除成功!");
+	}
+	
+	/**
+	 * 通过id查询
+	 *
+	 * @param id
+	 * @return
+	 */
+	//@AutoLog(value = "生产计划-通过id查询")
+	@ApiOperation(value="生产计划-通过id查询", notes="生产计划-通过id查询")
+	@GetMapping(value = "/queryById")
+	public Result<ProdPlan> queryById(@RequestParam(name="id",required=true) String id) {
+		ProdPlan prodPlan = prodPlanService.getById(id);
+		if(prodPlan==null) {
+			return Result.error("未找到对应数据");
+		}
+		return Result.OK(prodPlan);
+	}
+
+    /**
+    * 导出excel
+    *
+    * @param request
+    * @param prodPlan
+    */
+    @RequestMapping(value = "/exportXls")
+    public ModelAndView exportXls(HttpServletRequest request, ProdPlan prodPlan) {
+        return super.exportXls(request, prodPlan, ProdPlan.class, "生产计划");
+    }
+
+    /**
+      * 通过excel导入数据
+    *
+    * @param request
+    * @param response
+    * @return
+    */
+    @RequestMapping(value = "/importExcel", method = RequestMethod.POST)
+    public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
+		MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
+		Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
+		for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
+			// 获取上传文件对象
+			MultipartFile file = entity.getValue();
+		}
+		return Result.OK("文件导入失败!");
+    }
+
+	 @GetMapping(value = "/analysis")
+	 public Result<ProdPlan> analysis(@RequestParam(name="id",required=true) String id) {
+		 ProdPlan prodPlan = prodPlanService.getById(id);
+		 if(prodPlan==null) {
+			 return Result.error("未找到对应数据");
+		 }
+
+		 String filePath = uploadpath + File.separator + prodPlan.getPlanSheet();
+		 File file = new File(filePath);
+		 if(!file.exists()){
+			 log.error("文件["+prodPlan.getPlanSheet()+"]不存在..");
+			 return Result.error("文件不存在");
+		 }
+		 QueryWrapper<DelayProduct> delayProductWrapper = new QueryWrapper<>();
+		 delayProductWrapper.ge("pst", prodPlan.getPlanDate().plusDays(-1));
+		 delayProductService.remove(delayProductWrapper);
+		 List<DelayProduct> delayProducts = this.prodPlanService.parseExcel(filePath);
+		 if (delayProducts.size()>0) {
+			 delayProductService.saveBatch(delayProducts);
+			 return Result.OK("解析成功");
+		 }else{
+	         return Result.error("解析失败");
+		 }
+	 }
+
+	 @PostMapping(value = "/report")
+	 public ModelAndView report(@RequestBody ProdPlan prodPlan) {
+		 Workbook workbook = this.prodPlanService.report(prodPlan.getId(),prodPlan.getPie());
+		 //创建一个ModelAndView对象,并设置视图为AbstractView
+		 ModelAndView mv = new ModelAndView(new AbstractView() {
+			 //重写renderMergedOutputModel方法,设置响应头,将workbook写入响应流
+			 @Override
+			 protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
+				 response.setHeader("content-disposition", "attachment;filename=statistics.xlsx");
+				 ServletOutputStream out = response.getOutputStream();
+				 workbook.write(out);
+				 out.flush();
+			 }
+		 });
+		 //返回ModelAndView对象
+		 return mv;
+	 }
+
+	 @PostMapping(value = "/email")
+	 public Result<ProdPlan> email(@RequestBody ProdPlan prodPlan) {
+		 ProdPlan currentPlan = prodPlanService.getById(prodPlan.getId());
+		 if(currentPlan==null) {
+			 return Result.error("未找到对应数据");
+		 }
+		 Boolean success = this.prodPlanService.sendEmail(prodPlan.getId(),prodPlan.getPie());
+		 if (success) {
+			 return Result.OK("已发送");
+		 }else {
+			 return Result.error("发送失败");
+		 }
+	 }
+
+}

+ 77 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/entity/ProdPlan.java

@@ -0,0 +1,77 @@
+package org.jeecg.modules.ProdPlan.entity;
+
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.time.LocalDate;
+import java.util.Date;
+import java.math.BigDecimal;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import lombok.Data;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.jeecgframework.poi.excel.annotation.Excel;
+import org.jeecg.common.aspect.annotation.Dict;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+/**
+ * @Description: 生产计划
+ * @Author: jeecg-boot
+ * @Date:   2025-06-04
+ * @Version: V1.0
+ */
+@Data
+@TableName("lg_prod_plan")
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@ApiModel(value="lg_prod_plan对象", description="生产计划")
+public class ProdPlan implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+	/**主键*/
+	@TableId(type = IdType.ASSIGN_ID)
+    @ApiModelProperty(value = "主键")
+    private java.lang.String id;
+	/**创建人*/
+    @ApiModelProperty(value = "创建人")
+    private java.lang.String createBy;
+	/**创建日期*/
+	@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
+    @ApiModelProperty(value = "创建日期")
+    private java.util.Date createTime;
+	/**更新人*/
+    @ApiModelProperty(value = "更新人")
+    private java.lang.String updateBy;
+	/**更新日期*/
+	@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
+    @ApiModelProperty(value = "更新日期")
+    private java.util.Date updateTime;
+	/**所属部门*/
+    @ApiModelProperty(value = "所属部门")
+    private java.lang.String sysOrgCode;
+	/**计划日期*/
+	@Excel(name = "计划日期", width = 15, format = "yyyy-MM-dd")
+	@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
+    @DateTimeFormat(pattern="yyyy-MM-dd")
+    @ApiModelProperty(value = "计划日期")
+    private LocalDate planDate;
+	/**计划表*/
+	@Excel(name = "计划表", width = 15)
+    @ApiModelProperty(value = "计划表")
+    private java.lang.String planSheet;
+	/**发件状态*/
+	@Excel(name = "发件状态", width = 15)
+    @ApiModelProperty(value = "发件状态")
+    private java.lang.Integer sendState;
+
+
+    private transient byte[] pie;
+    private transient byte[] bar;
+}

+ 17 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/mapper/ProdPlanMapper.java

@@ -0,0 +1,17 @@
+package org.jeecg.modules.ProdPlan.mapper;
+
+import java.util.List;
+
+import org.apache.ibatis.annotations.Param;
+import org.jeecg.modules.ProdPlan.entity.ProdPlan;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * @Description: 生产计划
+ * @Author: jeecg-boot
+ * @Date:   2025-06-04
+ * @Version: V1.0
+ */
+public interface ProdPlanMapper extends BaseMapper<ProdPlan> {
+
+}

+ 5 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/mapper/xml/ProdPlanMapper.xml

@@ -0,0 +1,5 @@
+<?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="org.jeecg.modules.ProdPlan.mapper.ProdPlanMapper">
+
+</mapper>

+ 23 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/service/IProdPlanService.java

@@ -0,0 +1,23 @@
+package org.jeecg.modules.ProdPlan.service;
+
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.jeecg.modules.ProdPlan.entity.DelayProduct;
+import org.jeecg.modules.ProdPlan.entity.ProdPlan;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.List;
+
+/**
+ * @Description: 生产计划
+ * @Author: jeecg-boot
+ * @Date:   2025-06-04
+ * @Version: V1.0
+ */
+public interface IProdPlanService extends IService<ProdPlan> {
+
+    List<DelayProduct> parseExcel(String filePath);
+
+    XSSFWorkbook report(String id, byte[] pie);
+
+    Boolean sendEmail(String id, byte[] pie);
+}

+ 944 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/service/impl/ProdPlanServiceImpl.java

@@ -0,0 +1,944 @@
+package org.jeecg.modules.ProdPlan.service.impl;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.util.Units;
+import org.apache.poi.xssf.usermodel.XSSFPicture;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.jeecg.modules.Contact.entity.Contact;
+import org.jeecg.modules.Contact.service.IContactService;
+import org.jeecg.modules.DefectiveProduct.entity.DefectiveProduct;
+import org.jeecg.modules.DefectiveProduct.service.IDefectiveProductService;
+import org.jeecg.modules.DestRule.entity.DestRule;
+import org.jeecg.modules.DestRule.service.IDestRuleService;
+import org.jeecg.modules.ProdPlan.entity.DelayProduct;
+import org.jeecg.modules.ProdPlan.entity.DeliveredQuantity;
+import org.jeecg.modules.ProdPlan.entity.ProdPlan;
+import org.jeecg.modules.ProdPlan.mapper.ProdPlanMapper;
+import org.jeecg.modules.ProdPlan.service.IDelayProductService;
+import org.jeecg.modules.ProdPlan.service.IDeliveredQuantityService;
+import org.jeecg.modules.ProdPlan.service.IProdPlanService;
+import org.jeecg.modules.ProdPlan.util.EmailUtil;
+import org.jeecg.modules.ProdPlan.vo.ReportDetailVo;
+import org.jeecg.modules.ReasonRule.entity.ReasonRule;
+import org.jeecg.modules.ReasonRule.service.IReasonRuleService;
+import org.jeecg.modules.RegionRule.entity.RegionRule;
+import org.jeecg.modules.RegionRule.service.IRegionRuleService;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.IsoFields;
+import java.time.temporal.WeekFields;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * @Description: 生产计划
+ * @Author: jeecg-boot
+ * @Date:   2025-06-04
+ * @Version: V1.0
+ */
+@Service
+public class ProdPlanServiceImpl extends ServiceImpl<ProdPlanMapper, ProdPlan> implements IProdPlanService {
+    @Autowired
+    private IDelayProductService delayProductService;
+
+    @Autowired
+    private IRegionRuleService regionRuleService;
+
+    @Autowired
+    private IDestRuleService destRuleService;
+
+    @Autowired
+    private IReasonRuleService reasonRuleService;
+
+    @Autowired
+    private IDefectiveProductService defectiveProductService;
+
+    @Autowired
+    private IDeliveredQuantityService deliveredQuantityService;
+
+    @Autowired
+    private IContactService contactService;
+
+    @Override
+    public List<DelayProduct> parseExcel(String filePath) {
+        List<DelayProduct> delayProducts = new ArrayList<>();
+        try (FileInputStream fis = new FileInputStream(filePath)) {
+            Workbook workbook;
+            // 根据文件扩展名选择合适的工作簿类型
+            if (filePath.endsWith(".xlsx")) {
+                workbook = new XSSFWorkbook(fis);
+            } else if (filePath.endsWith(".xls")) {
+                workbook = new HSSFWorkbook(fis);
+            } else {
+                throw new IllegalArgumentException("不支持的文件格式,请使用.xlsx或.xls文件");
+            }
+            //第一张表
+            Sheet sheet = workbook.getSheetAt(0);
+            //第二行为表头
+            Row headerRow = sheet.getRow(1);
+            if (headerRow == null) {
+                throw new RuntimeException("未找到表头行");
+            }
+            //获取表头对应的列索引
+            Map<String, Integer> columnIndices = parseHeadRow(headerRow);
+            //获取表头中的日期列
+            Map<String, Integer> dateColumnIndices = parseDateColumn(columnIndices);
+            //从第三行开始读取数据
+            for (int i = 2; i <= sheet.getLastRowNum(); i++) {
+                Row row = sheet.getRow(i);
+                if(row==null) break;
+                if (row.getCell(columnIndices.get("Line"))==null) break;
+
+                parseDataRow(row, columnIndices, dateColumnIndices, delayProducts);
+            }
+
+            List<RegionRule> regionRuleList = regionRuleService.list();
+            Map<String, RegionRule> regionRules = regionRuleList.stream().collect(Collectors.toMap(RegionRule::getSuffix, i -> i));
+            List<DestRule> destRuleList = destRuleService.list();
+            Map<String, String> destRules = destRuleList.stream().collect(Collectors.toMap(DestRule::getDestination, DestRule::getSchedule));
+            List<ReasonRule> reasonRuleList = reasonRuleService.list();
+            Map<String, String> reasonRules = reasonRuleList.stream().collect(Collectors.toMap(ReasonRule::getComment2, ReasonRule::getRemark));
+
+            // 清洗数据,按规则匹配规定交付时间,计算超出时间,并转换延期原因
+            for (int i = 0; i < delayProducts.size(); i++) {
+                DelayProduct delayProduct = delayProducts.get(i);
+
+                //通过后缀匹配国别规则
+                String model = delayProduct.getModel().trim();
+                String suffix = model.substring(model.length() - 3);
+                RegionRule regionRule = regionRules.get(suffix);
+                if(regionRule==null) continue;
+                delayProduct.setRegion(regionRule.getRegion());
+
+                // 按国别规则匹配到期规则
+                String schecule = regionRule.getSchedule();
+                if("EU".equals(regionRule.getRegion())){
+                    //EU地区需要根据目的地匹配到期规则
+                    schecule = destRules.get(delayProduct.getFinalDest());
+                }
+                if(StringUtils.isBlank(schecule)) continue;
+                // 根据要求周别和到期规则,计算要求完成日期
+                LocalDate dueDate = getDueDate(delayProduct.getBucket(), schecule);
+                if(dueDate==null) continue;
+                delayProduct.setDueDate(dueDate);
+
+                // 计算计划生产日期超出要求完成日期天数
+                if (delayProduct.getPst() != null && delayProduct.getDueDate() != null) {
+                    int days = delayProduct.getDueDate().until(delayProduct.getPst()).getDays();
+//                    Long days = ChronoUnit.DAYS.between(delayProduct.getDueDate(), delayProduct.getPst());
+                    delayProduct.setOverdueDays(days);
+                }
+                // 计算要求完成周别
+                String week = "W" + delayProduct.getDueDate().get(WeekFields.ISO.weekOfYear());
+                delayProduct.setWeek(week);
+
+                reasonRules.forEach((reason,remark)->{
+                    Pattern pattern = Pattern.compile("^"+reason+"$");
+                    Matcher matcher = pattern.matcher(delayProduct.getComment2());
+                    if (matcher.matches()) {
+                        String newRemark = matcher.replaceAll(remark);
+                        delayProduct.setRemark(newRemark);
+                    }
+                });
+            }
+
+            List<DelayProduct> overDueList = delayProducts.stream().filter(i -> i.getRemark()!=null && i.getOverdueDays()!=null && i.getOverdueDays() > 0).collect(Collectors.toList());
+
+            return overDueList;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 获取表头对应的列索引
+     * @param headerRow
+     * @return
+     */
+    @NotNull
+    private Map<String, Integer> parseHeadRow(Row headerRow) {
+        Map<String, Integer> columnIndices = new LinkedHashMap<>();
+        for (Cell cell : headerRow) {
+            String headerValue = getCellValueAsString(cell);
+            columnIndices.put(headerValue, cell.getColumnIndex());
+        }
+        return columnIndices;
+    }
+
+    /**
+     * 获取表头中的日期列
+     * @param columnIndices
+     * @return
+     */
+    @NotNull
+    private Map<String, Integer> parseDateColumn(Map<String, Integer> columnIndices) {
+        Map<String, Integer> dateColumnIndices = new LinkedHashMap<>();
+        columnIndices.forEach((key, value) -> {
+            if (key.matches("\\d{2}/\\d{2}") || key.matches("\\d{1,2}/\\d{1,2}")) {
+                dateColumnIndices.put(key, value);
+            }
+        });
+        return dateColumnIndices;
+    }
+
+    /**
+     * 读取明细行数据
+     * @param row
+     * @param columnIndices
+     * @param dateColumnIndices
+     * @param delayProducts
+     */
+    private void parseDataRow(Row row, Map<String, Integer> columnIndices, Map<String, Integer> dateColumnIndices, List<DelayProduct> delayProducts) {
+        String Line = getCellValueAsString(row.getCell(columnIndices.get("Line")));
+        String demandId = getCellValueAsString(row.getCell(columnIndices.get("Demand ID")));
+        String tool = getCellValueAsString(row.getCell(columnIndices.get("Tool")));
+        String model = getCellValueAsString(row.getCell(columnIndices.get("Model.Suffix")));
+        String bucket = getCellValueAsString(row.getCell(columnIndices.get("Bucket")));
+        String finalDest = getCellValueAsString(row.getCell(columnIndices.get("Final Dest.")));
+        String comment1 = getCellValueAsString(row.getCell(columnIndices.get("Comment1")));
+        String comment2 = getCellValueAsString(row.getCell(columnIndices.get("Comment2")));
+
+        dateColumnIndices.forEach((date, columnIndex) -> {
+            Cell cell = row.getCell(columnIndex);
+            if (cell != null) {
+                String quantityStr = getCellValueAsString(cell);
+                if (quantityStr != null && !quantityStr.isEmpty()) {
+                    try {
+                        Integer quantity = Integer.parseInt(quantityStr);
+                        if (quantity > 0) {
+                            DelayProduct delayProduct =  new DelayProduct()
+                                    .setLine(Line)
+                                    .setDemandId(demandId)
+                                    .setTool(tool)
+                                    .setModel(model)
+                                    .setBucket(bucket)
+                                    .setFinalDest(finalDest)
+                                    .setComment1(comment1)
+                                    .setComment2(comment2)
+                                    .setPst(getDateFromMonthDayStr(date))
+                                    .setPo(quantity);
+                            delayProducts.add(delayProduct);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.error("无法解析数量: " + quantityStr, e);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * 获取单元格的值,作为字符串返回
+     * @param cell
+     * @return
+     */
+    private static String getCellValueAsString(Cell cell) {
+        if (cell == null) return null;
+
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue().trim();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    return cell.getDateCellValue().toString();
+                } else {
+                    // Remove decimal if it's .0
+                    double num = cell.getNumericCellValue();
+                    if (num == (long) num) {
+                        return String.valueOf((long) num);
+                    } else {
+                        return String.valueOf(num);
+                    }
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                switch (cell.getCachedFormulaResultType()) {
+                    case STRING:
+                        return cell.getStringCellValue().trim();
+                    case NUMERIC:
+                        return String.valueOf(cell.getNumericCellValue());
+                    case BOOLEAN:
+                        return String.valueOf(cell.getBooleanCellValue());
+                    default:
+                        return cell.getCellFormula();
+                }
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * 将日期列名字符串转换为日期
+     * @param monthDayStr
+     * @return
+     */
+    private static LocalDate getDateFromMonthDayStr(String monthDayStr) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd");
+        MonthDay monthDay = MonthDay.parse(monthDayStr, formatter);
+        LocalDate date = monthDay.atYear(LocalDate.now().getYear());
+        return date;
+    }
+
+    /**
+     * 通过周别和到期规则获取要求完成日期
+     * @param yearWeekStr
+     * @param schedule
+     * @return
+     */
+    private static LocalDate getDueDate(String yearWeekStr,String schedule) {
+        if (StringUtils.isBlank(yearWeekStr)||yearWeekStr.length() != 6) {
+            throw new IllegalArgumentException("周别格式必须为YYYYWW,如:202523");
+        }
+        int year = Integer.parseInt(yearWeekStr.substring(0, 4));
+        int week = Integer.parseInt(yearWeekStr.substring(4, 6));
+
+        Map<String, DayOfWeek> DAYS = new HashMap<>();
+        DAYS.put("MON", DayOfWeek.MONDAY);
+        DAYS.put("TUE", DayOfWeek.TUESDAY);
+        DAYS.put("WED", DayOfWeek.WEDNESDAY);
+        DAYS.put("THU", DayOfWeek.THURSDAY);
+        DAYS.put("FRI", DayOfWeek.FRIDAY);
+        DAYS.put("SAT", DayOfWeek.SATURDAY);
+        DAYS.put("SUN", DayOfWeek.SUNDAY);
+
+        boolean isLast = schedule.startsWith("Last ");
+        String dayStr = isLast ? schedule.substring(5).trim() : schedule;
+        if (isLast) {
+            week = week - 1;
+            if (week < 1) {
+                year = year - 1;
+                week = LocalDate.of(year, 12, 31).get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
+            }
+        }
+        DayOfWeek dayOfWeek = DAYS.get(dayStr);
+
+        return LocalDate.now()
+                .with(WeekFields.ISO.weekBasedYear(), year)
+                .with(WeekFields.ISO.weekOfWeekBasedYear(), week)
+                .with(WeekFields.ISO.dayOfWeek(), dayOfWeek.getValue());
+    }
+
+
+
+    @Override
+    public XSSFWorkbook report(String id, byte[] pie) {
+        ProdPlan prodPlan = this.getById(id);
+        int currentWeek = prodPlan.getPlanDate().get(WeekFields.ISO.weekOfYear());
+        List<ReportDetailVo> detailVos = new ArrayList<>();
+        // TODO 根据计划日期增加当前周期查询条件
+        List<DelayProduct> delayProductList = this.delayProductService.list();
+        delayProductList.forEach(delayProduct -> {
+            ReportDetailVo detailVo = new ReportDetailVo();
+            BeanUtils.copyProperties(delayProduct, detailVo);
+            detailVo.setPst(delayProduct.getPst().format(DateTimeFormatter.ofPattern("MM/dd")));
+            detailVo.setDueDate(delayProduct.getDueDate().format(DateTimeFormatter.ofPattern("MM/dd")));
+            LocalDate endDate = delayProduct.getPst();
+            LocalDate startDate = delayProduct.getDueDate();
+            while(endDate.isAfter(startDate)){
+                int startWeek = startDate.get(WeekFields.ISO.weekOfYear());
+                if(startWeek==currentWeek-3){
+                    detailVo.setWeek1(detailVo.getPo());
+                } else if (startWeek==currentWeek-2) {
+                    detailVo.setWeek2(detailVo.getPo());
+                } else if (startWeek==currentWeek-1) {
+                    detailVo.setWeek3(detailVo.getPo());
+                } else if (startWeek==currentWeek) {
+                    detailVo.setWeek4(detailVo.getPo());
+                } else if (startWeek==currentWeek+1) {
+                    detailVo.setWeek5(detailVo.getPo());
+                } else if (startWeek==currentWeek+2) {
+                    detailVo.setWeek6(detailVo.getPo());
+                }
+                startDate = startDate.plusWeeks(1l);
+            }
+            detailVos.add(detailVo);
+        });
+
+        List<DefectiveProduct> defectiveProductList = this.defectiveProductService.list();
+        defectiveProductList.forEach(defectiveProduct -> {
+            ReportDetailVo detailVo = new ReportDetailVo();
+            BeanUtils.copyProperties(defectiveProduct, detailVo);
+            detailVo.setPo(defectiveProduct.getPo());
+            detailVo.setDueDate(defectiveProduct.getDueDate().format(DateTimeFormatter.ofPattern("MM/dd")));
+            int week = Integer.valueOf(defectiveProduct.getWeek().substring(1));
+            if(week==currentWeek-3){
+                detailVo.setWeek1(detailVo.getPo());
+            } else if (week==currentWeek-2) {
+                detailVo.setWeek2(detailVo.getPo());
+            } else if (week==currentWeek-1) {
+                detailVo.setWeek3(detailVo.getPo());
+            } else if (week==currentWeek) {
+                detailVo.setWeek4(detailVo.getPo());
+            } else if (week==currentWeek+1) {
+                detailVo.setWeek5(detailVo.getPo());
+            } else if (week==currentWeek+2) {
+                detailVo.setWeek6(detailVo.getPo());
+            }
+            detailVos.add(detailVo);
+        });
+        List<DeliveredQuantity> deliveredQuantityList = this.deliveredQuantityService.list();
+        // TODO 按照计划日期,过滤影响到当前周的数据
+
+
+        XSSFWorkbook workbook=new XSSFWorkbook();
+        Sheet sheet = workbook.createSheet("生产数据统计");
+        // 隐藏网格线
+        sheet.setDisplayGridlines(false);
+        sheet.setPrintGridlines(false);
+        sheet.setDefaultRowHeightInPoints(20f);
+        try {
+            insertImageToSheetAtPosition(workbook,sheet,pie,"pie",0,0);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        // 创建各种样式
+        CellStyle darkHeaderStyle = createDarkHeaderStyle(workbook);
+        CellStyle subtotalStyle = createSubtotalStyle(workbook);
+        CellStyle dataStyle = createDataStyle(workbook);
+        CellStyle darkDataStyle = createDarkDataStyle(workbook);
+        CellStyle percentStyle = createPercentStyle(workbook);
+        CellStyle redPercentStyle = createRedPercentStyle(workbook);
+        CellStyle totalStyle = createTotalStyle(workbook);
+        CellStyle groupStyle = createGroupStyle(workbook);
+
+        int currentRow = 30;
+        int startColumn = 1;
+
+        // 第一部分:原因部门数据
+        currentRow = createFirstSection(currentWeek,sheet, currentRow,startColumn, darkHeaderStyle, subtotalStyle,
+                dataStyle, percentStyle, totalStyle, groupStyle);
+        List<ReportDetailVo> sumDetailVos = new ArrayList<>();
+        ReportDetailVo totalVo = getSummaryDetailVo(detailVos);
+        totalVo.setComment1("合计");
+
+        Map<String, List<ReportDetailVo>> deptMap = detailVos.stream().collect(Collectors.groupingBy(ReportDetailVo::getComment1,  Collectors.toList()));
+        deptMap.forEach((dept,deptList)->{
+            ReportDetailVo subTotalVo = getSummaryDetailVo(deptList);
+            subTotalVo.setComment1(dept);
+            subTotalVo.setLine("小计");
+            subTotalVo.setPercent((double)subTotalVo.getPo()/totalVo.getPo());
+
+            Map<String, Map<String, List<ReportDetailVo>>> lineMap = deptList.stream().collect(Collectors.groupingBy(ReportDetailVo::getLine, Collectors.groupingBy(ReportDetailVo::getTool, Collectors.toList())));
+            lineMap.forEach((line,toolMap)->{
+                toolMap.forEach((tool,toolList)->{
+                    ReportDetailVo toolVo = getSummaryDetailVo(toolList);
+                    toolVo.setComment1(dept);
+                    toolVo.setLine(line);
+                    toolVo.setTool(tool);
+                    toolVo.setPercent((double)toolVo.getPo()/subTotalVo.getPo());
+                    toolVo.setRemark(toolList.get(0).getRemark()+" "+toolList.get(0).getModel());
+                    sumDetailVos.add(toolVo);
+                });
+            });
+            sumDetailVos.add(subTotalVo);
+        } );
+        sumDetailVos.add(totalVo);
+
+
+
+        String currentDept = "";
+        String currentLine = "";
+        int deptStartRow = currentRow;
+        int lineStartRow = currentRow;
+        CellStyle cellStyle = dataStyle;
+        for(ReportDetailVo detailVo:sumDetailVos){
+            if("合计".equals(detailVo.getComment1())) {
+                cellStyle = totalStyle;
+            }else if("小计".equals(detailVo.getLine())) {
+                cellStyle = subtotalStyle;
+            }else {
+                cellStyle = dataStyle;
+            }
+            Row dataRow = sheet.createRow(currentRow++);
+            int currentColumn = startColumn;
+            createCell(dataRow,currentColumn++,detailVo.getComment1(),cellStyle);
+            createCell(dataRow,currentColumn++,"",cellStyle);
+            createCell(dataRow,currentColumn++,"",cellStyle);
+            if (!currentDept.equals(detailVo.getComment1())) {
+                if (!currentDept.isEmpty()) {
+                    sheet.addMergedRegion(new CellRangeAddress(deptStartRow, currentRow-2, startColumn, startColumn+2));
+                }
+                currentDept = detailVo.getComment1();
+                deptStartRow = currentRow-1;
+            }
+            if("合计".equals(detailVo.getComment1())){
+                sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn, startColumn+2));
+            }
+
+            createCell(dataRow,currentColumn++,detailVo.getLine(),cellStyle);
+            if (!currentLine.equals(detailVo.getLine())) {
+                if (!currentLine.isEmpty() &&currentRow-2>lineStartRow) {
+                    sheet.addMergedRegion(new CellRangeAddress(lineStartRow, currentRow-2, currentColumn-1, currentColumn-1));
+                }
+                currentLine = detailVo.getLine();
+                lineStartRow = currentRow-1;
+            }
+            createCell(dataRow,currentColumn++,detailVo.getTool(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getPo(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek3()==null?0:detailVo.getWeek3(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek4()==null?0:detailVo.getWeek4(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek5()==null?0:detailVo.getWeek5(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek6()==null?0:detailVo.getWeek6(),cellStyle);
+
+            if("合计".equals(detailVo.getComment1())) {
+                createCell(dataRow,currentColumn++,detailVo.getPercent()==null?0:detailVo.getPercent(),cellStyle);
+            }else if("小计".equals(detailVo.getLine())) {
+                createCell(dataRow,currentColumn++,detailVo.getPercent()==null?0:detailVo.getPercent(),redPercentStyle);
+            }else {
+                createCell(dataRow,currentColumn++,detailVo.getPercent()==null?0:detailVo.getPercent(),percentStyle);
+            }
+
+            createCell(dataRow,currentColumn++,detailVo.getRemark(),cellStyle);
+            sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, currentColumn-1, currentColumn+2));
+            createCell(dataRow,currentColumn++,"",cellStyle);
+            createCell(dataRow,currentColumn++,"",cellStyle);
+            createCell(dataRow,currentColumn++,"",cellStyle);
+        }
+
+
+        Map<String, Integer> deliveredQuantityMap = deliveredQuantityList.stream().collect(Collectors.toMap(DeliveredQuantity::getWeek, DeliveredQuantity::getQuantity));
+        Row quantityRow = sheet.createRow(currentRow++);
+        createCell(quantityRow,startColumn,"计划交付量",dataStyle);
+        sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn, startColumn+2));
+        createCell(quantityRow,startColumn+1,"",dataStyle);
+        createCell(quantityRow,startColumn+2,"",dataStyle);
+        createCell(quantityRow,startColumn+3,"",dataStyle);
+        createCell(quantityRow,startColumn+4,"",dataStyle);
+        createCell(quantityRow,startColumn+5,"",dataStyle);
+        createCell(quantityRow,startColumn+6,deliveredQuantityMap.get("W"+(currentWeek-1))==null?0:deliveredQuantityMap.get("W"+(currentWeek-1)),dataStyle);
+        createCell(quantityRow,startColumn+7,deliveredQuantityMap.get("W"+(currentWeek))==null?0:deliveredQuantityMap.get("W"+(currentWeek)),dataStyle);
+        createCell(quantityRow,startColumn+8,deliveredQuantityMap.get("W"+(currentWeek+1))==null?0:deliveredQuantityMap.get("W"+(currentWeek+1)),dataStyle);
+        createCell(quantityRow,startColumn+9,deliveredQuantityMap.get("W"+(currentWeek+2))==null?0:deliveredQuantityMap.get("W"+(currentWeek+2)),dataStyle);
+        createCell(quantityRow,startColumn+10,"",dataStyle);
+        createCell(quantityRow,startColumn+11,"",dataStyle);
+        createCell(quantityRow,startColumn+12,"",dataStyle);
+        createCell(quantityRow,startColumn+13,"",dataStyle);
+        createCell(quantityRow,startColumn+14,"",dataStyle);
+
+        Row lossrow = sheet.createRow(currentRow++);
+        createCell(lossrow,startColumn,"OTD0 Loss率",dataStyle);
+        sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn, startColumn+2));
+        createCell(lossrow,startColumn+3,"",dataStyle);
+        sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn+3, startColumn+5));
+        createCell(lossrow,startColumn+10,"",dataStyle);
+        sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn+10, startColumn+14));
+        Row otdrow = sheet.createRow(currentRow++);
+        createCell(otdrow,startColumn,"OTD0",dataStyle);
+        sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn, startColumn+2));
+        createCell(otdrow,startColumn+10,"",dataStyle);
+        sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn+10, startColumn+14));
+        createCell(otdrow,startColumn+3,"",dataStyle);
+        sheet.addMergedRegion(new CellRangeAddress(currentRow-1, currentRow-1, startColumn+3, startColumn+5));
+        createCell(lossrow,startColumn+1,"",dataStyle);
+        createCell(lossrow,startColumn+2,"",dataStyle);
+        createCell(lossrow,startColumn+3,"",dataStyle);
+        createCell(lossrow,startColumn+4,"",dataStyle);
+        createCell(lossrow,startColumn+5,"",dataStyle);
+        createCell(lossrow,startColumn+10,"",dataStyle);
+        createCell(lossrow,startColumn+11,"",dataStyle);
+        createCell(lossrow,startColumn+12,"",dataStyle);
+        createCell(lossrow,startColumn+13,"",dataStyle);
+        createCell(lossrow,startColumn+14,"",dataStyle);
+        if(totalVo.getWeek3()!=null && deliveredQuantityMap.get("W" + (currentWeek - 1))!=null) {
+            createCell(lossrow, startColumn+6, (double)totalVo.getWeek3()/ deliveredQuantityMap.get("W" + (currentWeek - 1)), percentStyle);
+            createCell(otdrow, startColumn+6, 1-(double)totalVo.getWeek3()/ deliveredQuantityMap.get("W" + (currentWeek - 1)), percentStyle);
+        }else{
+
+            createCell(lossrow, startColumn+6, "", percentStyle);
+            createCell(otdrow, startColumn+6, "", percentStyle);
+        }
+        if(totalVo.getWeek4()!=null && deliveredQuantityMap.get("W" + (currentWeek))!=null) {
+            createCell(lossrow, startColumn+7, (double)totalVo.getWeek4()/ deliveredQuantityMap.get("W" + (currentWeek)), percentStyle);
+            createCell(otdrow, startColumn+7, 1-(double)totalVo.getWeek4()/ deliveredQuantityMap.get("W" + (currentWeek)), percentStyle);
+        }
+        if(totalVo.getWeek5()!=null && deliveredQuantityMap.get("W" + (currentWeek+1))!=null) {
+            createCell(lossrow, startColumn+8, (double)totalVo.getWeek5()/ deliveredQuantityMap.get("W" + (currentWeek+1)), percentStyle);
+            createCell(otdrow, startColumn+8, 1-(double)totalVo.getWeek5()/ deliveredQuantityMap.get("W" + (currentWeek+1)), percentStyle);
+        }
+        if(totalVo.getWeek6()!=null && deliveredQuantityMap.get("W" + (currentWeek+2))!=null) {
+            createCell(lossrow, startColumn+9, (double)totalVo.getWeek6()/ deliveredQuantityMap.get("W" + (currentWeek+2)), percentStyle);
+            createCell(otdrow, startColumn+9, 1-(double)totalVo.getWeek6()/ deliveredQuantityMap.get("W" + (currentWeek+2)), percentStyle);
+        }
+
+        // 第二部分:产品需求数据
+        currentRow = createSecondSection(currentWeek,sheet, currentRow,startColumn, darkHeaderStyle, subtotalStyle, dataStyle, groupStyle);
+        cellStyle = darkDataStyle;
+        for(ReportDetailVo detailVo:detailVos){
+            Row dataRow = sheet.createRow(currentRow++);
+            int currentColumn = startColumn;
+            cellStyle = cellStyle.equals(dataStyle) ? darkDataStyle : dataStyle;
+            createCell(dataRow,currentColumn++,detailVo.getRegion(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getModel(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getLine(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getTool(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getPo(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek3()==null?0:detailVo.getWeek3(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek4()==null?0:detailVo.getWeek4(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek5()==null?0:detailVo.getWeek5(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getWeek6()==null?0:detailVo.getWeek6(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getPst(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getDueDate(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getOverdueDays()==null?0:detailVo.getOverdueDays(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getComment1(),cellStyle);
+            createCell(dataRow,currentColumn++,detailVo.getRemark(),cellStyle);
+        }
+
+//        // 自动调整列宽
+        autoSizeColumns(sheet, startColumn+15);
+        return workbook;
+    }
+
+    @NotNull
+    private ReportDetailVo getSummaryDetailVo(List<ReportDetailVo> detailVos) {
+        Integer poQty = detailVos.stream().map(ReportDetailVo::getPo).filter(Objects::nonNull).reduce(0, Integer::sum);
+        Integer week1 = detailVos.stream().map(ReportDetailVo::getWeek1).filter(Objects::nonNull).reduce(0, Integer::sum);
+        Integer week2 = detailVos.stream().map(ReportDetailVo::getWeek2).filter(Objects::nonNull).reduce(0, Integer::sum);
+        Integer week3 = detailVos.stream().map(ReportDetailVo::getWeek3).filter(Objects::nonNull).reduce(0, Integer::sum);
+        Integer week4 = detailVos.stream().map(ReportDetailVo::getWeek4).filter(Objects::nonNull).reduce(0, Integer::sum);
+        Integer week5 = detailVos.stream().map(ReportDetailVo::getWeek5).filter(Objects::nonNull).reduce(0, Integer::sum);
+        Integer week6 = detailVos.stream().map(ReportDetailVo::getWeek6).filter(Objects::nonNull).reduce(0, Integer::sum);
+        ReportDetailVo totalVo = new ReportDetailVo();
+        totalVo.setPo(poQty);
+        totalVo.setWeek1(week1);
+        totalVo.setWeek2(week2);
+        totalVo.setWeek3(week3);
+        totalVo.setWeek4(week4);
+        totalVo.setWeek5(week5);
+        totalVo.setWeek6(week6);
+        return totalVo;
+    }
+    private void insertImageToSheetAtPosition(Workbook workbook, Sheet sheet, byte[] imageBytes,
+                                              String fileName, int startRow, int startCol) throws Exception {
+        int pictureIdx = workbook.addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG);
+        Drawing drawing = sheet.createDrawingPatriarch();
+        ClientAnchor anchor = workbook.getCreationHelper().createClientAnchor();
+
+        // 设置图片位置
+        anchor.setCol1(startCol);
+        anchor.setRow1(startRow);
+        anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
+
+        XSSFPicture picture = (XSSFPicture)drawing.createPicture(anchor, pictureIdx);
+        picture.resize();
+    }
+
+    /**
+     * 创建第一部分数据表格
+     */
+    private static int createFirstSection(int currentWeek,Sheet sheet, int startRow, int startColumn, CellStyle darkHeaderStyle,
+                                          CellStyle subtotalStyle, CellStyle dataStyle,
+                                          CellStyle percentStyle, CellStyle totalStyle,
+                                          CellStyle groupStyle) {
+        int currentRow = startRow;
+
+        // 创建表头
+        Row headerRow1 = sheet.createRow(currentRow++);
+        String[] headers1 = {"原因部门", "", "", "Line", "Tool", "PO Qty", "周别累计影响数量", "", "", "", "%", "Remark"};
+        for (int i = 0; i < headers1.length; i++) {
+            Cell cell = headerRow1.createCell(startColumn+i);
+            cell.setCellValue(headers1[i]);
+            cell.setCellStyle(darkHeaderStyle);
+        }
+
+        // 第二行表头
+        Row headerRow2 = sheet.createRow(currentRow++);
+        String[] headers2 = {"", "", "", "", "", "", "W"+(currentWeek-1), "W"+currentWeek, "W"+(currentWeek+1), "W"+(currentWeek+2), "", ""};
+        for (int i = 0; i < headers2.length; i++) {
+            Cell cell = headerRow2.createCell(startColumn+i);
+            cell.setCellValue(headers2[i]);
+            cell.setCellStyle(darkHeaderStyle);
+        }
+
+        // 合并单元格
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn, startColumn+2)); // 原因部门
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+3, startColumn+3)); // Line
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+4, startColumn+4)); // Tool
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+5, startColumn+5)); // PO Qty
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow, startColumn+6, startColumn+9)); // 周别累计影响数量
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+10, startColumn+10)); // %
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+11, startColumn+14)); // Remark
+
+        return currentRow;
+    }
+
+    /**
+     * 创建第二部分数据表格
+     */
+    private static int createSecondSection(int currentWeek,Sheet sheet, int startRow, int startColumn,CellStyle darkHeaderStyle,
+                                            CellStyle subtotalStyle, CellStyle dataStyle,
+                                            CellStyle groupStyle) {
+        int currentRow = startRow;
+
+        // 创建第二个表格的表头
+        Row headerRow = sheet.createRow(currentRow++);
+        String[] headers = {"产品需求", "","", "Line", "Tool", "PO", "周别累计影响数量", "", "","",
+                "当前日期", "要求完成日期", "超出天数", "原因部门", "Remark"};
+        for (int i = 0; i < headers.length; i++) {
+            Cell cell = headerRow.createCell(startColumn+i);
+            cell.setCellValue(headers[i]);
+            cell.setCellStyle(darkHeaderStyle);
+        }
+
+        // 第二行表头
+        Row headerRow2 = sheet.createRow(currentRow++);
+        String[] headers2 = { "Region","LG Model","Week", "", "", "", "W"+(currentWeek-1), "W"+currentWeek, "W"+(currentWeek+1), "W"+(currentWeek+2), "", "", "", "", ""};
+        for (int i = 0; i < headers2.length; i++) {
+            Cell cell = headerRow2.createCell(startColumn+i);
+            cell.setCellValue(headers2[i]);
+            cell.setCellStyle(darkHeaderStyle);
+        }
+
+        // 合并单元格
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow , startColumn, startColumn+2)); // 产品需求
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+3, startColumn+3)); // Line
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+4, startColumn+4)); // Tool
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+5, startColumn+5)); // PO Qty
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow, startColumn+6, startColumn+9)); // 周别累计影响数量
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+10, startColumn+10)); // 当前日期
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+11, startColumn+11)); // 要求完成日期
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+12, startColumn+12)); // 超出天数
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+13, startColumn+13)); // 原因部门
+        sheet.addMergedRegion(new CellRangeAddress(startRow, startRow + 1, startColumn+14, startColumn+14)); // Remark
+
+        return currentRow;
+    }
+
+    /**
+     * 创建深色表头样式
+     */
+    private static CellStyle createDarkHeaderStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+
+        // 设置深灰色背景
+        style.setFillForegroundColor(IndexedColors.GREY_80_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+
+        // 设置白色字体
+        Font font = workbook.createFont();
+        font.setColor(IndexedColors.WHITE.getIndex());
+        font.setBold(true);
+        font.setFontHeightInPoints((short) 12);
+        style.setFont(font);
+
+        // 设置边框
+        style.setBorderTop(BorderStyle.NONE);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.NONE);
+        style.setBorderRight(BorderStyle.NONE);
+
+        // 设置对齐
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+
+        return style;
+    }
+
+    /**
+     * 创建小计样式
+     */
+    private static CellStyle createSubtotalStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+
+        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+
+        Font font = workbook.createFont();
+        font.setBold(true);
+        font.setFontHeightInPoints((short) 12);
+        style.setFont(font);
+
+        style.setBorderTop(BorderStyle.NONE);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.NONE);
+        style.setBorderRight(BorderStyle.NONE);
+
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+
+        return style;
+    }
+
+    /**
+     * 创建合计样式
+     */
+    private static CellStyle createTotalStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+
+        style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+
+        Font font = workbook.createFont();
+        font.setBold(true);
+        font.setFontHeightInPoints((short) 12);
+        style.setFont(font);
+
+        style.setBorderTop(BorderStyle.NONE);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.NONE);
+        style.setBorderRight(BorderStyle.NONE);
+
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+
+        return style;
+    }
+
+    /**
+     * 创建数据样式
+     */
+    private static CellStyle createDataStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+
+        style.setBorderTop(BorderStyle.NONE);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.NONE);
+        style.setBorderRight(BorderStyle.NONE);
+
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+
+        Font font = workbook.createFont();
+        font.setFontHeightInPoints((short) 11);
+        style.setFont(font);
+
+        return style;
+    }
+    private static CellStyle createDarkDataStyle(Workbook workbook) {
+        CellStyle style = createDataStyle(workbook);
+
+        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+        return style;
+    }
+
+    /**
+     * 创建百分比样式
+     */
+    private static CellStyle createPercentStyle(Workbook workbook) {
+        CellStyle style = createDataStyle(workbook);
+        DataFormat format = workbook.createDataFormat();
+        style.setDataFormat(format.getFormat("0.0%"));
+        Font font = workbook.createFont();
+        font.setBold(true);
+        font.setFontHeightInPoints((short) 12);
+        style.setFont(font);
+
+        return style;
+    }
+    private static CellStyle createRedPercentStyle(Workbook workbook) {
+        CellStyle style = createSubtotalStyle(workbook);
+        DataFormat format = workbook.createDataFormat();
+        style.setDataFormat(format.getFormat("0.0%"));
+        Font font = workbook.createFont();
+        font.setColor(IndexedColors.RED.getIndex());
+        font.setBold(true);
+        font.setFontHeightInPoints((short) 12);
+        style.setFont(font);
+
+        return style;
+    }
+
+    /**
+     * 创建分组样式
+     */
+    private static CellStyle createGroupStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+
+        style.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+
+        Font font = workbook.createFont();
+        font.setBold(true);
+        font.setFontHeightInPoints((short) 12);
+        style.setFont(font);
+
+
+        style.setBorderTop(BorderStyle.NONE);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.NONE);
+        style.setBorderRight(BorderStyle.NONE);
+
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+
+        return style;
+    }
+
+    private static void createCell(Row row, int column, String value, CellStyle style) {
+        Cell cell = row.createCell(column);
+        cell.setCellValue(value);
+        cell.setCellStyle(style);
+    }
+
+    private static void createCell(Row row, int column, int value, CellStyle style) {
+        Cell cell = row.createCell(column);
+        if(value>0) cell.setCellValue(value);
+        cell.setCellStyle(style);
+    }
+
+    private static void createCell(Row row, int column, double value, CellStyle style) {
+        Cell cell = row.createCell(column);
+        if(value>0) cell.setCellValue(value);
+        cell.setCellStyle(style);
+    }
+
+    /**
+     * 自动调整列宽
+     */
+    private static void autoSizeColumns(Sheet sheet, int maxColumns) {
+        for (int i = 0; i < maxColumns; i++) {
+            sheet.autoSizeColumn(i);
+            // 设置最小和最大宽度
+            int currentWidth = sheet.getColumnWidth(i);
+            if (currentWidth < 2000) {
+                sheet.setColumnWidth(i, 2000); // 最小宽度
+            } else if (currentWidth > 8000) {
+                sheet.setColumnWidth(i, 8000); // 最大宽度
+            }
+        }
+    }
+
+
+
+    @Override
+    public Boolean sendEmail(String id, byte[] pie) {
+        ProdPlan prodPlan = this.getById(id);
+        String date = prodPlan.getPlanDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+        List<Contact> list = contactService.list();
+        List<String> contactsTO = list.stream().filter(i -> "Y".equals(i.getIsActive()) && "TO".equals(i.getRecType())).map(Contact::getEmail).collect(Collectors.toList());
+        List<String> contactsCC = list.stream().filter(i -> "Y".equals(i.getIsActive()) && "CC".equals(i.getRecType())).map(Contact::getEmail).collect(Collectors.toList());
+
+        Workbook workbook = this.report(id, pie);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        byte[] excelBytes = null;
+        try {
+            workbook.write(outputStream);
+            excelBytes = outputStream.toByteArray();
+            workbook.close();
+            Boolean success = EmailUtil.sendEmailWithExcelAttachment(contactsTO, contactsCC, "生产异常追踪表_" + date, "生产异常追踪表_" + date, excelBytes);
+            return success;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 103 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/util/EmailUtil.java

@@ -0,0 +1,103 @@
+package org.jeecg.modules.ProdPlan.util;
+
+import com.sun.mail.util.MailSSLSocketFactory;
+
+import javax.mail.*;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import java.security.GeneralSecurityException;
+import java.util.Date;
+import java.util.List;
+import java.util.Properties;
+
+public class EmailUtil {
+
+    /**
+     * 设置邮件,发送excel附件
+     *
+     * @param recipientTo 接收者
+     * @param recipientCc 抄送者
+     * @param subject   邮件标题
+     * @param content   邮件内容
+     * @param excelBytes excel附件
+     */
+    public static Boolean sendEmailWithExcelAttachment(List<String> recipientTo, List<String> recipientCc, String subject, String content, byte[] excelBytes) {
+        String username = "shudianfapiao@eguancloud.com";
+        String password = "TaHMQE2egJm5R5bg";
+        Properties prop = new Properties();
+        //协议
+        prop.setProperty("mail.transport.protocol", "smtp");
+        //服务器
+        prop.setProperty("mail.smtp.host", "smtp.exmail.qq.com");
+        //端口
+        prop.setProperty("mail.smtp.port", "465");
+        //使用smtp身份验证
+        prop.setProperty("mail.smtp.auth", "true");
+        //企业邮箱必须要SSL
+        prop.put("mail.smtp.ssl.enable", "true");
+        MailSSLSocketFactory sf = null;
+        try {
+            sf = new MailSSLSocketFactory();
+            sf.setTrustAllHosts(true);
+        } catch (GeneralSecurityException e1) {
+            e1.printStackTrace();
+        }
+        prop.put("mail.smtp.ssl.socketFactory", sf);
+        //获取Session对象
+        Session s = Session.getDefaultInstance(prop, new Authenticator() {
+            //此访求返回用户和密码的对象
+            @Override
+            protected PasswordAuthentication getPasswordAuthentication() {
+                PasswordAuthentication pa = new PasswordAuthentication(username, password);
+                return pa;
+            }
+        });
+        //设置session的调试模式,发布时取消
+        s.setDebug(true);
+        MimeMessage mimeMessage = new MimeMessage(s);
+        try {
+            mimeMessage.setFrom(new InternetAddress(username));//发送邮箱账号
+            //发送多个人
+            Address[] addressTo = new Address[recipientTo.size()];
+            for (int i = 0; i < recipientTo.size(); i++) {
+                addressTo[i] = new InternetAddress(recipientTo.get(i));
+            }
+            //抄送多个人
+            Address[] addressCc = new Address[recipientCc.size()];
+            for (int i = 0; i < recipientCc.size(); i++) {
+                addressCc[i] = new InternetAddress(recipientCc.get(i));
+            }
+
+            mimeMessage.addRecipients(Message.RecipientType.TO, addressTo);//接受者
+            mimeMessage.addRecipients(Message.RecipientType.CC, addressCc);//抄送
+            //设置主题
+            mimeMessage.setSubject(subject);//邮件标题
+            mimeMessage.setSentDate(new Date());
+            //设置内容
+            BodyPart messageBodyPart = new MimeBodyPart();
+            messageBodyPart.setContent(content,"text/html;charset=UTF-8");
+
+            //附件
+            MimeBodyPart attachmentPart = new MimeBodyPart();
+            attachmentPart.setFileName(subject+".xlsx");
+            attachmentPart.setContent(excelBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+
+            // 创建多重消息
+            Multipart multipart = new MimeMultipart();
+            // 设置文本消息部分
+            multipart.addBodyPart(messageBodyPart);
+            multipart.addBodyPart(attachmentPart);
+
+            mimeMessage.setContent(multipart);
+            mimeMessage.saveChanges();
+            //发送
+            Transport.send(mimeMessage);
+            return true;
+        } catch (MessagingException e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+}

+ 55 - 0
lg-code/src/main/java/org/jeecg/modules/ProdPlan/vo/ReportDetailVo.java

@@ -0,0 +1,55 @@
+package org.jeecg.modules.ProdPlan.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.jeecgframework.poi.excel.annotation.Excel;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDate;
+
+@Data
+public class ReportDetailVo {
+    /**region*/
+    @Excel(name = "region", width = 15)
+    private String region;
+    /**model*/
+    @Excel(name = "model", width = 15)
+    private String model;
+    /**week*/
+    @Excel(name = "week", width = 15)
+    private String week;
+    /**line*/
+    @Excel(name = "line", width = 15)
+    private String line;
+    /**tool*/
+    @Excel(name = "tool", width = 15)
+    private String tool;
+    /**po*/
+    @Excel(name = "po", width = 15)
+    private Integer po;
+
+    private Integer week1;
+    private Integer week2;
+    private Integer week3;
+    private Integer week4;
+    private Integer week5;
+    private Integer week6;
+
+    private String pst;
+    private String dueDate;
+    /**Overdue Days*/
+    @Excel(name = "Overdue Days", width = 15)
+    @ApiModelProperty(value = "Overdue Days")
+    private java.lang.Integer overdueDays;
+    /**Comment1*/
+    @Excel(name = "Comment1", width = 15)
+    @ApiModelProperty(value = "Comment1")
+    private java.lang.String comment1;
+    /**Remark*/
+    @Excel(name = "Remark", width = 15)
+    @ApiModelProperty(value = "Remark")
+    private java.lang.String remark;
+
+    private Double percent;
+}