diff --git a/pom.xml b/pom.xml index 796092e..7596720 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 1.39.0 3.5.3.1 3.9.1 - 5.8.18 + 5.8.30 4.10.0 2.7.10 3.20.1 diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/poi/ExcelTemplateProc.java b/ruoyi-common/src/main/java/com/ruoyi/common/poi/ExcelTemplateProc.java index 1f7180d..aea8b27 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/poi/ExcelTemplateProc.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/poi/ExcelTemplateProc.java @@ -293,9 +293,10 @@ public class ExcelTemplateProc { String value = cell.getStringCellValue(); if (value != null) { - + String trimmed = value.trim(); for (DynamicDataMapping dynamicData : dynamicDataMappingList) { - if (value.startsWith("{{" + dynamicData.getDataId() + ".")) { + String prefix = "{{" + dynamicData.getDataId() + "."; + if (trimmed.startsWith(prefix) || trimmed.contains(prefix)) { return dynamicData; } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/PartCostController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/PartCostController.java index e148f09..20e5452 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/PartCostController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/PartCostController.java @@ -3,6 +3,7 @@ package com.ruoyi.system.controller; import java.util.List; import java.util.Arrays; +import com.xxl.job.core.handler.annotation.XxlJob; import lombok.RequiredArgsConstructor; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.*; @@ -113,6 +114,7 @@ public class PartCostController extends BaseController { @Log(title = "在金蝶获取成本价", businessType = BusinessType.INSERT) @RepeatSubmit() @PostMapping("/getObtainPartData") + @XxlJob("getObtainPartData") public R getObtainPartData() { return toAjax(iPartCostService.getObtainPartData()); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java index 7205dad..8d9250e 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java @@ -6,6 +6,7 @@ import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.*; +import cn.hutool.json.JSONUtil; import com.alibaba.excel.EasyExcel; import com.fasterxml.jackson.core.JsonProcessingException; import com.ruoyi.common.excel.DefaultExcelListener; @@ -17,6 +18,8 @@ import com.ruoyi.common.poi.ExcelTemplateProc; import com.ruoyi.common.poi.DynamicDataMapping; import com.ruoyi.system.domain.*; import com.ruoyi.system.domain.bo.FigureSaveBo; +import com.ruoyi.system.domain.dto.MaterialUseDTO; +import com.ruoyi.system.domain.dto.ProcessRouteDTO; import com.ruoyi.system.domain.dto.ProcessRouteExcelDTO; import com.ruoyi.system.domain.dto.ProcessRoutePushResultDTO; import com.ruoyi.system.domain.vo.*; @@ -542,7 +545,7 @@ public class ProcessOrderProController extends BaseController { // 使用Excel模板文件 - String templatePath = "D:/java/excel-template/生产及工艺计划模版.xlsx"; + String templatePath = "jpg/生产及工艺计划模版.xlsx"; String outputPath = "D:/file/" + orderPro.getProductionOrderNo() + "生产及工艺计划表.xlsx"; // 准备模板数据 @@ -613,11 +616,11 @@ public class ProcessOrderProController extends BaseController { List> evoDataList = convertEVOProductsDataToMapList(evoProductsList); dynamicDataMappingList.addAll(DynamicDataMapping.createOneDataList("EVOProductsDataVO", evoDataList)); } - // 添加伊特产品数据 + /* // 添加伊特产品数据 if (!excelDTOList.isEmpty()) { List> evoRouteDataList = convertRouteDataToMapList(excelDTOList); dynamicDataMappingList.addAll(DynamicDataMapping.createOneDataList("ProcessRouteExcelDTO", evoRouteDataList)); - } + }*/ // 使用模板导出Excel ExcelTemplateProc.doExportExcelByTemplateProc(templatePath, outputPath, staticDataMap, dynamicDataMappingList); @@ -625,8 +628,7 @@ public class ProcessOrderProController extends BaseController { // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); - String fileName = URLEncoder.encode(orderPro.getProductionOrderNo() + "_分类BOM表", "UTF-8") - .replaceAll("\\+", "%20"); + String fileName = URLEncoder.encode(orderPro.getProductionOrderNo() + "_分类BOM表", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); // 将生成的文件写入响应 @@ -671,7 +673,15 @@ public class ProcessOrderProController extends BaseController { vo.setMaterialCode(getCellValueAsString(row.getCell(0))); // 图号 vo.setMaterialName(getCellValueAsString(row.getCell(1))); // 名称 vo.setMaterial(getCellValueAsString(row.getCell(2))); // 数量 - vo.setDiscWeight(getCellValueAsDouble(row.getCell(3))); // 单重 + vo.setDiscWeight(getCellValueAsDouble(row.getCell(3))); + + vo.setRawMaterialCode(getCellValueAsString(row.getCell(4))); + vo.setRawMaterialName(getCellValueAsString(row.getCell(5))); + vo.setBomMaterial(getCellValueAsString(row.getCell(6))); + vo.setBomUnit(getCellValueAsString(row.getCell(9))); + vo.setBomDanZhong(getCellValueAsDouble(row.getCell(7))); + vo.setDiscUsage(getCellValueAsString(row.getCell(8))); + // 单重 vo.setBatchQuantity(getCellValueAsLong(row.getCell(18))); // 批次数量 vo.setUnitQuantity(getCellValueAsDouble(row.getCell(17))); // 批次数量 @@ -1092,10 +1102,10 @@ public class ProcessOrderProController extends BaseController { /** * 转换工艺VO为Map列表(用于模板) */ - private List> convertRouteDataToMapList(List routeDataList) { + private List> convertRouteDataToMapList(List routeDataList) { List> mapList = new ArrayList<>(); int index = 1; - for (ProcessRouteExcelDTO item : routeDataList) { + for (ProcessRoute item : routeDataList) { Map map = new HashMap<>(); map.put("index", index); map.put("routeDescription", item.getRouteDescription()); // 生产令号 @@ -1103,12 +1113,6 @@ public class ProcessOrderProController extends BaseController { map.put("materialName", item.getMaterialName()); // 物料名称 map.put("material", item.getMaterial()); // 材质 map.put("discWeight", item.getDiscWeight()); // 单重KG - map.put("rawMaterialCode", item.getRawMaterialCode()); // 材料BOM物料编码 - map.put("rawMaterialName", item.getRawMaterialName()); // 材料BOM物料名称 - map.put("bomMaterial", item.getBomMaterial()); // BOM材质 - map.put("bomDanZhong", item.getBomDanZhong()); // 材料单重KG - map.put("discUsage", item.getDiscUsage()); // 用量 - map.put("bomUnit", item.getBomUnit()); // 单位 map.put("processNo", item.getProcessNo()); // 工序号 map.put("workCenter", item.getWorkCenter()); // 工作中心 map.put("processName", item.getProcessName()); // 工序名称 @@ -1153,14 +1157,78 @@ public class ProcessOrderProController extends BaseController { // 1. 读取第一个sheet的数据list - 使用POI直接读取以保留空格 List allDataList = readExcelWithPOI(excelName); List routeList = readExcelPOIRoute(excelName); - //获取此项目的物料最新BOM 并且 将工艺路线拉取出来 - ArrayList routes = new ArrayList<>(); - for (ProcessRoute processRoute : routeList) { - //TODO 获取最新的BOM版本的型号 - String bomVersion = JdUtil.readGetTheLatestVersion(processRoute.getMaterialCode()); - //TODO 获取此物料的所有工艺路线 - + List routes = new ArrayList<>(); + List> kingdeeBomRows = new ArrayList<>(); + for (ProcessRoute base : routeList) { + String materialCode = base.getMaterialCode(); + if (StringUtils.isBlank(materialCode)) { + ProcessRoute item = new ProcessRoute(); + item.setRouteDescription(base.getRouteDescription()); + item.setMaterialCode(base.getMaterialCode()); + item.setMaterialName(base.getMaterialName()); + item.setMaterial(base.getMaterial()); + item.setDiscWeight(base.getDiscWeight()); + item.setUnitQuantity(base.getUnitQuantity()); + item.setBatchQuantity(base.getBatchQuantity()); + routes.add(item); + continue; + } + String bomversion = JdUtil.readGetTheLatestVersion(materialCode); + List bomItems = StringUtils.isNotBlank(bomversion) ? JdUtil.getMaterialUseXByVer(bomversion) : Collections.emptyList(); + List routeGuDing = JdUtil.getRouteGuDing(materialCode); + if (bomItems != null && !bomItems.isEmpty()) { + for (MaterialUseDTO b : bomItems) { + Map bomMap = new HashMap<>(); + bomMap.put("routeDescription", base.getRouteDescription()); + bomMap.put("materialCode", base.getMaterialCode()); + bomMap.put("materialName", base.getMaterialName()); + bomMap.put("material", base.getMaterial()); + bomMap.put("discWeight", base.getDiscWeight()); + bomMap.put("rawMaterialCode", b.getMaterialCode()); + bomMap.put("rawMaterialName", b.getMaterialName()); + bomMap.put("bomMaterial", b.getCaizhi()); + bomMap.put("bomDanZhong", b.getDanzhong()); + bomMap.put("discUsage", (b.getFenzi() != null && b.getFenmu() != null) ? (b.getFenzi() + "/" + b.getFenmu()) : null); + bomMap.put("bomUnit", b.getChildUnit()); + kingdeeBomRows.add(bomMap); + } + } + if (routeGuDing != null && !routeGuDing.isEmpty()) { + routeGuDing.stream() + .forEach(r -> { + ProcessRoute item = new ProcessRoute(); + item.setRouteDescription(base.getRouteDescription()); + item.setMaterialCode(base.getMaterialCode()); + item.setMaterialName(base.getMaterialName()); + item.setMaterial(base.getMaterial()); + item.setDiscWeight(base.getDiscWeight()); + item.setUnitQuantity(base.getUnitQuantity()); + item.setBatchQuantity(base.getBatchQuantity()); + // 不写入BOM字段,保持纯工艺数据行 + item.setProcessNo(r.getProcessNo()); + item.setWorkCenter(r.getWorkCenter()); + item.setProcessName(r.getProcessName()); + item.setProcessDescription(r.getProcessDescription()); + item.setProcessControl(r.getProcessControl()); + item.setActivityDuration(r.getActivityDuration()); + item.setActivityUnit(r.getActivityUnit()); + routes.add(item); + }); + } else { + ProcessRoute item = new ProcessRoute(); + item.setRouteDescription(base.getRouteDescription()); + item.setMaterialCode(base.getMaterialCode()); + item.setMaterialName(base.getMaterialName()); + item.setMaterial(base.getMaterial()); + item.setDiscWeight(base.getDiscWeight()); + item.setUnitQuantity(base.getUnitQuantity()); + item.setBatchQuantity(base.getBatchQuantity()); + // 不写入BOM字段,保持纯工艺数据行 + routes.add(item); + } } + // 用生成的 routes 替换原始 routeList,保持原序展开后的结构用于后续导出 + routeList = routes; // 2. 读取原始表数据 List rawDataList = readRawDataTable(rawDataFile); @@ -1311,7 +1379,7 @@ public class ProcessOrderProController extends BaseController { } // 使用Excel模板文件 - String templatePath = "D:/java/excel-template/生产及工艺计划模版.xlsx"; + String templatePath = "jpg/生产及工艺计划模版.xlsx"; String outputPath = "D:/file/" + orderPro.getProductionOrderNo() + "生产及工艺计划表.xlsx"; // 准备模板数据 @@ -1383,11 +1451,15 @@ public class ProcessOrderProController extends BaseController { List> evoDataList = convertEVOProductsDataToMapList(evoProductsList); dynamicDataMappingList.addAll(DynamicDataMapping.createOneDataList("EVOProductsDataVO", evoDataList)); } + if (!kingdeeBomRows.isEmpty()) { + List> bomDataList2 = convertKingdeeBomToMapList(kingdeeBomRows); + dynamicDataMappingList.addAll(DynamicDataMapping.createOneDataList("KingdeeBomData", bomDataList2)); + } // 添加伊特产品数据 - /*if (!excelDTOList.isEmpty()) { - List> evoRouteDataList = convertRouteDataToMapList(excelDTOList); + if (!routeList.isEmpty()) { + List> evoRouteDataList = convertRouteDataToMapList(routeList); dynamicDataMappingList.addAll(DynamicDataMapping.createOneDataList("ProcessRouteExcelDTO", evoRouteDataList)); - }*/ + } // 使用模板导出Excel ExcelTemplateProc.doExportExcelByTemplateProc(templatePath, outputPath, staticDataMap, dynamicDataMappingList); @@ -1421,16 +1493,28 @@ public class ProcessOrderProController extends BaseController { } } - - - - - - - - - - + private List> convertKingdeeBomToMapList(List> kingdeeBomRows) { + List> mapList = new ArrayList<>(); + int index = 1; + for (Map row : kingdeeBomRows) { + Map map = new HashMap<>(); + map.put("index", index); + map.put("routeDescription", row.get("routeDescription")); + map.put("materialCode", row.get("materialCode")); + map.put("materialName", row.get("materialName")); + map.put("material", row.get("material")); + map.put("discWeight", row.get("discWeight")); + map.put("rawMaterialCode", row.get("rawMaterialCode")); + map.put("rawMaterialName", row.get("rawMaterialName")); + map.put("bomMaterial", row.get("bomMaterial")); + map.put("bomDanZhong", row.get("bomDanZhong")); + map.put("discUsage", row.get("discUsage")); + map.put("bomUnit", row.get("bomUnit")); + mapList.add(map); + index++; + } + return mapList; + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/JdVersionDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/JdVersionDTO.java index d866682..60b8ebe 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/JdVersionDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/JdVersionDTO.java @@ -1,5 +1,10 @@ package com.ruoyi.system.domain.dto; -public class JdVersionDTO { +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +@Data +public class JdVersionDTO { + @JsonProperty("FNumber") + private String version; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java index a292b80..1db5393 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java @@ -12,6 +12,7 @@ import com.kingdee.bos.webapi.entity.RepoRet; import com.kingdee.bos.webapi.sdk.K3CloudApi; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.utils.JdUtils; +import com.ruoyi.common.utils.VersionComparator; import com.ruoyi.system.domain.BomDetails; import com.ruoyi.system.domain.PartCost; import com.ruoyi.system.domain.dto.*; @@ -2488,7 +2489,6 @@ public class JdUtil { /** * 获取物料清单 - * * @param FMaterialCode * @return */ @@ -3232,68 +3232,104 @@ public class JdUtil { */ public static String readGetTheLatestVersion(String materialCode) { K3CloudApi client = new K3CloudApi(); - // 请求参数,要求为json字符串 JsonObject json = new JsonObject(); - json.addProperty("FormId", "SFC_OperationReport"); - json.addProperty("FieldKeys", "F_HBYT_SCLH,FBillNo,FMoNumber,FWorkShopID.FName,FOperNumber,FOperDescription,FQuaQty,FFinishQty,FStockInQuaAuxQty,FStockInFailAuxQty,F_HBYT_RKD,FDate"); - // 是否为入库点 - JsonObject filterObject = new JsonObject(); + json.addProperty("FormId", "ENG_BOM"); + json.addProperty("FieldKeys", "FNumber"); JsonArray filterString = new JsonArray(); - filterObject.addProperty("FieldName", "F_HBYT_RKD"); - filterObject.addProperty("Compare", "74"); - filterObject.addProperty("Value", true); + JsonObject filterObject = new JsonObject(); + filterObject.addProperty("FieldName", "FMATERIALID.FNumber"); + filterObject.addProperty("Compare", "67"); + filterObject.addProperty("Value", materialCode); filterObject.addProperty("Left", ""); filterObject.addProperty("Right", ""); filterObject.addProperty("Logic", 0); filterString.add(filterObject); JsonObject filterObject1 = new JsonObject(); - filterObject1.addProperty("FieldName", "FDocumentStatus"); + filterObject1.addProperty("FieldName", "FForbidStatus"); filterObject1.addProperty("Compare", "105"); filterObject1.addProperty("Value", "A"); filterObject1.addProperty("Left", ""); filterObject1.addProperty("Right", ""); - filterObject1.addProperty("Logic", 1); + filterObject1.addProperty("Logic", 0); filterString.add(filterObject1); - JsonObject filterObject2 = new JsonObject(); - filterObject2.addProperty("FieldName", "FDocumentStatus"); - filterObject2.addProperty("Compare", "105"); - filterObject2.addProperty("Value", "B"); - filterObject2.addProperty("Left", ""); - filterObject2.addProperty("Right", ""); - filterObject2.addProperty("Logic", 1); - filterString.add(filterObject2); json.add("FilterString", filterString); json.addProperty("OrderString", ""); json.addProperty("TopRowCount", 0); json.addProperty("StartRow", 0); - json.addProperty("Limit", 10000); + json.addProperty("Limit", 2000); json.addProperty("SubSystemId", ""); - List processReportDTOList = new ArrayList<>(); - int pageSize = 10000; - int startRow = 0; + List processReportDTOList = new ArrayList<>(); ObjectMapper objectMapper = new ObjectMapper(); try { - while (true) { - JsonObject pageJson = new Gson().fromJson(json.toString(), JsonObject.class); - pageJson.addProperty("StartRow", startRow); - pageJson.addProperty("Limit", pageSize); - String resultJson = String.valueOf(client.billQuery(pageJson.toString())); - JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class); - if (jsonArray == null || jsonArray.size() == 0) { - break; - } - List pageList = objectMapper.readValue(jsonArray.toString(), new TypeReference>() { + String resultJson = String.valueOf(client.billQuery(json.toString())); + JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class); + if (jsonArray != null && jsonArray.size() > 0) { + List pageList = objectMapper.readValue(jsonArray.toString(), new TypeReference>() { }); processReportDTOList.addAll(pageList); - if (jsonArray.size() < pageSize) { - break; - } - startRow += pageSize; } } catch (Exception e) { - e.printStackTrace(); // 输出异常日志 + e.printStackTrace(); } + if (processReportDTOList.isEmpty()) { + return null; + } + String latest = processReportDTOList.stream() + .map(JdVersionDTO::getVersion) + .filter(Objects::nonNull) + .max(new VersionComparator()) + .orElse(null); + return latest; + } + /** + * 根据bom版本获取物料清单 + * @param version + * @return + */ + public static List getMaterialUseXByVer(String version) { + K3CloudApi client = new K3CloudApi(); + // 请求参数,要求为json字符串 + JsonObject json = new JsonObject(); + json.addProperty("FormId", "ENG_BOM"); + json.addProperty("FieldKeys", "FNumber,FMATERIALIDCHILD.FNumber,FCHILDITEMNAME,FCHILDUNITID.FName,FNUMERATOR,FDENOMINATOR,F_HBYT_DZ,F_HBYT_CZ"); + JsonArray filterString = new JsonArray(); + JsonObject filterObject = new JsonObject(); + filterObject.addProperty("FieldName", "FNumber"); + filterObject.addProperty("Compare", "67"); + filterObject.addProperty("Value", version); + filterObject.addProperty("Left", ""); + filterObject.addProperty("Right", ""); + filterObject.addProperty("Logic", 0); + filterString.add(filterObject); + JsonObject filterObject1 = new JsonObject(); + filterObject1.addProperty("FieldName", "FForbidStatus"); + filterObject1.addProperty("Compare", "105"); + filterObject1.addProperty("Value", "A"); + filterObject1.addProperty("Left", ""); + filterObject1.addProperty("Right", ""); + filterObject1.addProperty("Logic", 0); + filterString.add(filterObject1); + json.add("FilterString", filterString); + json.addProperty("OrderString", ""); + json.addProperty("TopRowCount", 0); + json.addProperty("StartRow", 0); + json.addProperty("Limit", 2000); + json.addProperty("SubSystemId", ""); - return " "; + String jsonData = json.toString(); + try { + String resultJson = String.valueOf(client.billQuery(jsonData)); + JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class); + if (jsonArray != null && jsonArray.size() > 0) { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(jsonArray.toString(), new TypeReference>() { + }); + } else { + return Collections.emptyList(); + } + } catch (Exception e) { + log.error("调用接口时发生异常: " + e.getMessage(), e); + } + return Collections.emptyList(); } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PartCostServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PartCostServiceImpl.java index 55f9ccd..c2820af 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PartCostServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PartCostServiceImpl.java @@ -124,24 +124,18 @@ public class PartCostServiceImpl implements IPartCostService { Map uniq = new LinkedHashMap<>(); for (PartCost pc : list) { if (pc == null) continue; - String code = pc.getMaterialCode(); String name = pc.getMaterialName(); BigDecimal cost = pc.getCostPrice(); - // 过滤无效数据 if (StringUtils.isBlank(code) || StringUtils.isBlank(name) || cost == null || cost.compareTo(BigDecimal.ZERO) == 0) { continue; } - // 以 物料编码 + 名称 为唯一键 String key = code + "|" + name; - PartCost existing = uniq.get(key); // 保留 createDate 最新的 - if (existing == null || - (pc.getCreateDate() != null && - (existing.getCreateDate() == null || pc.getCreateDate().after(existing.getCreateDate())))) { + if (existing == null || (pc.getCreateDate() != null && (existing.getCreateDate() == null || pc.getCreateDate().after(existing.getCreateDate())))) { uniq.put(key, pc); } } diff --git a/ruoyi-system/src/main/resources/EXCEL模板/生产及工艺计划模版.xlsx b/ruoyi-system/src/main/resources/EXCEL模板/生产及工艺计划模版.xlsx index 82479b5..4e8f264 100644 Binary files a/ruoyi-system/src/main/resources/EXCEL模板/生产及工艺计划模版.xlsx and b/ruoyi-system/src/main/resources/EXCEL模板/生产及工艺计划模版.xlsx differ diff --git a/ruoyi-system/src/main/resources/jpg/生产及工艺计划模版.xlsx b/ruoyi-system/src/main/resources/jpg/生产及工艺计划模版.xlsx new file mode 100644 index 0000000..caaf06d Binary files /dev/null and b/ruoyi-system/src/main/resources/jpg/生产及工艺计划模版.xlsx differ