更新,工序计划
This commit is contained in:
parent
e5aab8b9a7
commit
c71f6ecc98
@ -161,6 +161,22 @@
|
||||
<version>1.39.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-core</artifactId>
|
||||
<version>1.39.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-core</artifactId>
|
||||
<version>1.39.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.google.ortools/ortools-java -->
|
||||
|
||||
@ -9,6 +9,7 @@ import com.ruoyi.common.dingding.DingCallbackCrypto;
|
||||
import com.ruoyi.common.dingding.robot.RobotGroupService;
|
||||
import com.ruoyi.common.dingding.util.DingUtil;
|
||||
import com.ruoyi.common.dingding.webhook.WebhookClient;
|
||||
import com.ruoyi.common.dingding.workflow.SchemeApplicationBpmsEventHandler;
|
||||
import com.ruoyi.system.controller.KingdeeWorkCenterDataController;
|
||||
import com.ruoyi.system.domain.ProcessOrderPro;
|
||||
import com.ruoyi.system.mapper.ProcessOrderProMapper;
|
||||
@ -49,6 +50,9 @@ public class CallbackController {
|
||||
@Autowired
|
||||
private WebhookClient webhookClient; // 注入 WebhookClient
|
||||
|
||||
@Autowired
|
||||
private SchemeApplicationBpmsEventHandler schemeApplicationBpmsEventHandler;
|
||||
|
||||
@PostMapping("/receive")
|
||||
public Map<String, String> callBack(@RequestBody(required = false) Map<String, Object> json) {
|
||||
|
||||
@ -216,6 +220,8 @@ public class CallbackController {
|
||||
eventType = "robot_message";
|
||||
}
|
||||
bizLogger.info("事件类型: {}", eventType);
|
||||
//只处理「技术方案表」且「同意」场景
|
||||
schemeApplicationBpmsEventHandler.handleEventPayload(eventType, eventJson.toJSONString());
|
||||
// 4. 根据EventType分类处理
|
||||
if ("check_url".equals(eventType)) {
|
||||
bizLogger.info("测试回调url的正确性");
|
||||
|
||||
@ -2,12 +2,14 @@ package com.ruoyi.web.controller.dingtalk;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.dingding.relay.DingMessageRelayService;
|
||||
import com.ruoyi.common.dingding.util.DingUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ -18,6 +20,8 @@ import com.dingtalk.api.response.OapiV2UserGetResponse;
|
||||
@RequestMapping("/ding/talk")
|
||||
public class DingTalkController {
|
||||
|
||||
private final DingMessageRelayService dingMessageRelayService;
|
||||
|
||||
/**
|
||||
* 查询用户详情
|
||||
*/
|
||||
@ -136,4 +140,28 @@ public class DingTalkController {
|
||||
|
||||
return R.ok("发送成功", processQueryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例:经消息中转平台发送 Markdown 单聊(需启动中转服务并配置 dingtalk.message-relay-base-url)
|
||||
*/
|
||||
@GetMapping("/testRelayMarkdown")
|
||||
public R<String> testRelayMarkdown() {
|
||||
StringBuilder markdownMsg = new StringBuilder();
|
||||
markdownMsg.append("### 安全库存提醒\n\n")
|
||||
.append("今日暂无安全库存预警数据。\n\n")
|
||||
.append("**物料编号: 123321**\n")
|
||||
.append("- 物料名称: 测试测试567\n")
|
||||
.append("- 预计可用量: 200\n")
|
||||
.append("- 即时库存: 100\n")
|
||||
.append("- 最大库存: 50\n")
|
||||
.append("---\n")
|
||||
.append("**总数汇总**\n")
|
||||
.append("- 今日安全库存预警数据总数: **50**\n");
|
||||
|
||||
String resp = dingMessageRelayService.sendMarkdownToUser(
|
||||
"test测试",
|
||||
markdownMsg.toString(),
|
||||
Arrays.asList("史国臣", "田志阳"));
|
||||
return R.ok("中转返回", resp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,8 +49,10 @@ spring:
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
#url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://192.168.5.121:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||
# 本机双实例:5.7 常用 3306,8.0 常用其它端口(示例 3307);内网库另见下行注释
|
||||
#url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
#url: jdbc:mysql://localhost:3307/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
url: jdbc:mysql://192.168.5.121:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
# 从库数据源
|
||||
@ -58,11 +60,13 @@ spring:
|
||||
lazy: true
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
#url: jdbc:mysql://localhost:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://192.168.5.121:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||
#url: jdbc:mysql://localhost:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
#url: jdbc:mysql://localhost:3307/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
url: jdbc:mysql://192.168.5.121:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
sqlserver:
|
||||
lazy: true
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
url: jdbc:sqlserver://192.168.5.9:1433;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true
|
||||
|
||||
@ -30,7 +30,6 @@ server:
|
||||
servlet:
|
||||
# 应用的访问路径
|
||||
context-path: /
|
||||
undertow:
|
||||
# HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
|
||||
max-http-post-size: -1
|
||||
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
|
||||
@ -71,7 +70,7 @@ spring:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
profiles:
|
||||
active: '@profiles.active@'
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
# 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
@ -99,6 +98,15 @@ spring:
|
||||
# 允许对象忽略json中不存在的属性
|
||||
fail_on_unknown_properties: false
|
||||
|
||||
plm:
|
||||
pdf:
|
||||
base-dir: D:/plm/pdf
|
||||
separator: ""
|
||||
auth:
|
||||
app-key: plm
|
||||
app-secret: change-me
|
||||
username: plm_bot
|
||||
|
||||
# Sa-Token配置
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
@ -296,6 +304,8 @@ management:
|
||||
logfile:
|
||||
external-file: ./logs/sys-console.log
|
||||
dingtalk:
|
||||
# 钉钉消息中转服务地址(本服务 POST {base}/ding/send)
|
||||
message-relay-base-url: http://192.168.5.121:8089
|
||||
app-key: dingebfrpzqko25vo8w6
|
||||
app-secret: M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk
|
||||
robot-code: dingebfrpzqko25vo8w6
|
||||
@ -310,3 +320,15 @@ dingtalk:
|
||||
route-approval-id: 01201038164023402380
|
||||
plan-user-id: 313514580637707245,192530665123549546
|
||||
sync-k3-notify-user-ids: 313514580637707245,351225426738742993,192530665123549546
|
||||
# 技术方案申请表 OA 模板 processCode(钉钉后台复制);留空则不处理 BPMS 事件
|
||||
scheme-application-process-code: PROC-81A79B4D-F0EA-4353-9A91-68FD7E3BCEBB
|
||||
# 整单审批结束且 result=agree 时拉取表单(推荐,与 event.json 顶层 result=agree 一致)
|
||||
scheme-bpms-on-instance-agree: true
|
||||
# 单审批任务结束且 result=agree 时拉取(会多次触发;任务节点可能出现 audit 等非 agree)
|
||||
scheme-bpms-on-task-agree: false
|
||||
# 日志中只输出这些表单控件名称(逗号分隔);留空则输出全部非空字段
|
||||
scheme-application-log-field-names: 方案提交日期,是否收到方案,任务详情
|
||||
|
||||
pdf:
|
||||
# PDF字体(支持 classpath: 前缀)
|
||||
font-path: classpath:font/arial unicode ms.ttf
|
||||
|
||||
@ -78,6 +78,31 @@ public class DingTalkProperties {
|
||||
*/
|
||||
private String syncK3NotifyUserIds;
|
||||
|
||||
/**
|
||||
* 消息中转服务根地址(如 http://host:8089,不要带尾斜杠也可),为空则不调用中转发送
|
||||
*/
|
||||
private String messageRelayBaseUrl;
|
||||
|
||||
/**
|
||||
* 技术方案申请表 OA 模板 processCode(如 PROC-xxx);为空则不处理 BPMS 审批事件
|
||||
*/
|
||||
private String schemeApplicationProcessCode = "";
|
||||
|
||||
/**
|
||||
* 是否在 bpms_instance_change 整单结束且 result=agree 时拉取审批详情(推荐)
|
||||
*/
|
||||
private boolean schemeBpmsOnInstanceAgree = true;
|
||||
|
||||
/**
|
||||
* 是否在 bpms_task_change 单任务结束且 result=agree 时拉取审批详情(会多次触发,默认关闭)
|
||||
*/
|
||||
private boolean schemeBpmsOnTaskAgree = false;
|
||||
|
||||
/**
|
||||
* 拉取详情后日志中输出的控件名称(逗号分隔);为空则输出全部非空表单字段
|
||||
*/
|
||||
private String schemeApplicationLogFieldNames = "";
|
||||
|
||||
/**
|
||||
* 回调路由 Key
|
||||
*/
|
||||
|
||||
@ -11,15 +11,19 @@ import org.springframework.stereotype.Component;
|
||||
import shade.com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 钉钉 Stream 模式事件推送服务
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DingTalkStreamService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DingTalkStreamService.class);
|
||||
|
||||
private final com.ruoyi.common.dingding.workflow.SchemeApplicationBpmsEventHandler schemeApplicationBpmsEventHandler;
|
||||
|
||||
// 钉钉应用的 AppKey
|
||||
private static final String CLIENT_ID = "dingebfrpzqko25vo8w6";
|
||||
// 钉钉应用的 AppSecret
|
||||
@ -57,6 +61,7 @@ public class DingTalkStreamService {
|
||||
log.info("BornTime: {}", bornTime);
|
||||
log.info("BizData: {}", bizData != null ? bizData.toString() : "null");
|
||||
|
||||
schemeApplicationBpmsEventHandler.handleEventPayload(eventType, bizData != null ? bizData.toJSONString() : "{}");
|
||||
//处理事件
|
||||
process(bizData);
|
||||
//消费成功
|
||||
|
||||
@ -934,7 +934,14 @@ public class DingUtil {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 使用 Token 初始化账号 Client(OA 审批 / workflow)
|
||||
*/
|
||||
public static com.aliyun.dingtalkworkflow_1_0.Client createWorkflowClient() throws Exception {
|
||||
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
|
||||
config.protocol = "https";
|
||||
config.regionId = "central";
|
||||
return new com.aliyun.dingtalkworkflow_1_0.Client(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ spring:
|
||||
application:
|
||||
name: ruoyi-monitor-admin
|
||||
profiles:
|
||||
active: @profiles.active@
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
|
||||
logging:
|
||||
config: classpath:logback-plus.xml
|
||||
|
||||
@ -9,12 +9,14 @@ spring.boot.admin.client:
|
||||
username: ruoyi
|
||||
password: 123456
|
||||
|
||||
--- # 数据库配置
|
||||
--- # 数据库配置(与 ruoyi-admin dev 同一实例;本机 5.7 多为 3306,MySQL8 多为 3307)
|
||||
spring:
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
|
||||
#url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
url: jdbc:mysql://localhost:3307/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
#url: jdbc:mysql://192.168.5.121:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
hikari:
|
||||
|
||||
@ -7,7 +7,7 @@ spring:
|
||||
application:
|
||||
name: ruoyi-xxl-job-admin
|
||||
profiles:
|
||||
active: @profiles.active@
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
mvc:
|
||||
servlet:
|
||||
load-on-startup: 0
|
||||
|
||||
@ -84,6 +84,8 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
private final IProcessOrderProService iProcessOrderProService;
|
||||
|
||||
private final IProductionOrderService iProductionOrderService;
|
||||
|
||||
private final ProcessOrderProMapper processOrderProMapper;
|
||||
|
||||
private final BomDetailsMapper bomDetailsMapper;
|
||||
@ -390,12 +392,17 @@ public class BomDetailsController extends BaseController {
|
||||
if (bomDetails != null && !bomDetails.isEmpty()) {
|
||||
//TODO 处理父级物料
|
||||
ProcessOrderPro bo = iProcessOrderProService.selectByProjectNumber(totalWeight);
|
||||
|
||||
// 第一步:处理所有物料的保存
|
||||
for (BomDetails material : bomDetails) {
|
||||
// 获取工艺表中的非委外工时
|
||||
Double fbWorkTime = iProcessRouteService.getFbWorkTime(material);
|
||||
if (material.getPartNumber() != null && material.getName() != null && material.getUnitWeight().equals("否")) {
|
||||
if (material.getPartNumber().startsWith("015") || material.getPartNumber().startsWith("009")) {
|
||||
return R.fail("015和009为管控物料,请先申请");
|
||||
}
|
||||
if (material.getRemarks() != null && material.getRemarks().contains("甲供")) {
|
||||
continue;
|
||||
}
|
||||
String state = determineState(material);
|
||||
//判断bom类型是生产还是电气
|
||||
if (material.getBomType().equals("0")) {
|
||||
@ -928,7 +935,20 @@ public class BomDetailsController extends BaseController {
|
||||
JsonObject FSVRIAssistant = new JsonObject();
|
||||
FSVRIAssistant.addProperty("FNumber", materialProperties.getMaterialAttributeId());
|
||||
model.add("F_SVRI_Assistant", FSVRIAssistant);
|
||||
|
||||
// 单重:与 Excel 批量匹配逻辑一致(production_order.drawing_no → process_route.material_code);图号用物料编码 partNumber;库中未查到则为 null
|
||||
Double dz = iProductionOrderService.resolveSingleWeightByMaterialCode(bomDetails.getPartNumber());
|
||||
if (dz == null) {
|
||||
if (bomDetails.getDanZhong() != null && bomDetails.getSingleWeghit() != null) {
|
||||
dz = bomDetails.getSingleWeghit();
|
||||
} else if (bomDetails.getDanZhong() != null) {
|
||||
dz = bomDetails.getDanZhong();
|
||||
} else if (bomDetails.getSingleWeghit() != null) {
|
||||
dz = bomDetails.getSingleWeghit();
|
||||
}
|
||||
}
|
||||
if (dz != null) {
|
||||
model.addProperty("F_HBYT_DZ", dz);
|
||||
}
|
||||
// 创建FMaterialGroup对象,并加入Model
|
||||
JsonObject fMaterialGroup = new JsonObject();
|
||||
fMaterialGroup.addProperty("FNumber", "XM-100");
|
||||
@ -946,6 +966,9 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
// 创建FCategoryID对象,并加入SubHeadEntity
|
||||
JsonObject fCategoryID = new JsonObject();
|
||||
if(bomDetails.getFNumber().contains("-MP")){
|
||||
fCategoryID.addProperty("FNumber", "007");
|
||||
}
|
||||
fCategoryID.addProperty("FNumber", "CHLB05_SYS");
|
||||
subHeadEntity.add("FCategoryID", fCategoryID);
|
||||
|
||||
@ -1094,7 +1117,7 @@ public class BomDetailsController extends BaseController {
|
||||
/*
|
||||
* FB父级物料保存接口
|
||||
*/
|
||||
public static int loadFBMaterialPreservation(BomDetails bomDetail) {
|
||||
public int loadFBMaterialPreservation(BomDetails bomDetail) {
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
// 创建一个空的JsonObject
|
||||
JsonObject json = new JsonObject();
|
||||
@ -1117,7 +1140,20 @@ public class BomDetailsController extends BaseController {
|
||||
model.add("FMaterialGroup", fMaterialGroup);
|
||||
|
||||
model.addProperty("FIsHandleReserve", true);
|
||||
|
||||
// 单重:与 Excel 批量匹配逻辑一致(production_order.drawing_no → process_route.material_code);图号用物料编码 partNumber;库中未查到则为 null
|
||||
Double dz = iProductionOrderService.resolveSingleWeightByMaterialCode(bomDetail.getPartNumber());
|
||||
if (dz == null) {
|
||||
if (bomDetail.getDanZhong() != null && bomDetail.getSingleWeghit() != null) {
|
||||
dz = bomDetail.getSingleWeghit();
|
||||
} else if (bomDetail.getDanZhong() != null) {
|
||||
dz = bomDetail.getDanZhong();
|
||||
} else if (bomDetail.getSingleWeghit() != null) {
|
||||
dz = bomDetail.getSingleWeghit();
|
||||
}
|
||||
}
|
||||
if (dz != null) {
|
||||
model.addProperty("F_HBYT_DZ", dz);
|
||||
}
|
||||
// 创建SubHeadEntity对象,并加入Model
|
||||
JsonObject subHeadEntity = new JsonObject();
|
||||
model.add("SubHeadEntity", subHeadEntity);
|
||||
@ -1127,6 +1163,9 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
// 创建FCategoryID对象,并加入SubHeadEntity
|
||||
JsonObject fCategoryID = new JsonObject();
|
||||
if (bomDetail.getFNumber().contains("-MP")){
|
||||
fCategoryID.addProperty("FNumber", "007");//
|
||||
}
|
||||
fCategoryID.addProperty("FNumber", "CHLB05_SYS");// 产成品
|
||||
subHeadEntity.add("FCategoryID", fCategoryID);
|
||||
|
||||
@ -1512,11 +1551,19 @@ public class BomDetailsController extends BaseController {
|
||||
json.add("Model", model);
|
||||
|
||||
model.addProperty("FMATERIALID", 0);
|
||||
//单重
|
||||
if (!(bomDetails1.getDanZhong() == null) && !(bomDetails1.getSingleWeghit() == null)) {
|
||||
model.addProperty("F_HBYT_DZ", bomDetails1.getSingleWeghit());
|
||||
} else {
|
||||
model.addProperty("F_HBYT_DZ", bomDetails1.getDanZhong());
|
||||
// 单重:与 Excel 批量匹配逻辑一致(production_order.drawing_no → process_route.material_code);图号用物料编码 partNumber;库中未查到则为 null
|
||||
Double dz = iProductionOrderService.resolveSingleWeightByMaterialCode(bomDetails1.getPartNumber());
|
||||
if (dz == null) {
|
||||
if (bomDetails1.getDanZhong() != null && bomDetails1.getSingleWeghit() != null) {
|
||||
dz = bomDetails1.getSingleWeghit();
|
||||
} else if (bomDetails1.getDanZhong() != null) {
|
||||
dz = bomDetails1.getDanZhong();
|
||||
} else if (bomDetails1.getSingleWeghit() != null) {
|
||||
dz = bomDetails1.getSingleWeghit();
|
||||
}
|
||||
}
|
||||
if (dz != null) {
|
||||
model.addProperty("F_HBYT_DZ", dz);
|
||||
}
|
||||
model.addProperty("FSpecification", bomDetails1.getRemarks());
|
||||
model.addProperty("FNumber", bomDetails1.getPartNumber());
|
||||
@ -1544,9 +1591,9 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
subHeadEntity.addProperty("FErpClsID", states);
|
||||
subHeadEntity.addProperty("FFeatureItem", "1");
|
||||
|
||||
//存货类别
|
||||
JsonObject fCategoryID = new JsonObject();
|
||||
if (states.equals("2") || states.equals("3")) {
|
||||
if (states.equals("2") || states.equals("3")||bomDetails1.getPartNumber().contains("-MP")) {
|
||||
fCategoryID.addProperty("FNumber", "007");
|
||||
} else {
|
||||
fCategoryID.addProperty("FNumber", "006");
|
||||
@ -2260,7 +2307,7 @@ public class BomDetailsController extends BaseController {
|
||||
List<BomDetails> list = new ArrayList<>();
|
||||
if (rows != null) {
|
||||
for (ElectricalMaterialBomVO bomVO : rows) {
|
||||
if(bomVO.getRemark().equals("研发已领用")|| bomVO.getRemark().contains("领用")){
|
||||
if (bomVO.getRemark() != null && bomVO.getRemark().contains("领用")) {
|
||||
continue;
|
||||
}
|
||||
BomDetails bomDetails = new BomDetails();
|
||||
@ -2271,6 +2318,9 @@ public class BomDetailsController extends BaseController {
|
||||
bomDetails.setName(bomVO.getDrawingName());
|
||||
bomDetails.setMaterial(bomVO.getModel());
|
||||
bomDetails.setWareHouse(bomVO.getBrand());
|
||||
if(bomVO.getBrand().contains("伊特")){
|
||||
bomDetails.setStats("自制");
|
||||
}
|
||||
bomDetails.setStats("外购");
|
||||
bomDetails.setQuantity(bomVO.getQuantity() == null ? null : bomVO.getQuantity().toString());
|
||||
bomDetails.setRemarks(bomVO.getUnit());
|
||||
@ -2320,6 +2370,7 @@ public class BomDetailsController extends BaseController {
|
||||
}
|
||||
return materialsToAdd;
|
||||
}
|
||||
|
||||
@Log(title = "查看BOM上传日志")
|
||||
@SaCheckPermission("system:route:viewGetBomUploadStatus")
|
||||
@PostMapping("/viewGetBomUploadStatus")
|
||||
|
||||
@ -13,9 +13,13 @@ import java.util.stream.Collectors;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
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.kingdee.bos.webapi.entity.SuccessEntity;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.excel.ExcelResult;
|
||||
import com.ruoyi.common.utils.JdUtils;
|
||||
import com.ruoyi.system.domain.EleMaterials;
|
||||
@ -453,43 +457,103 @@ public class EleMaterialsController extends BaseController {
|
||||
}
|
||||
|
||||
|
||||
@Log(title = "更新物料单重", businessType = BusinessType.IMPORT)
|
||||
@Log(title = "更新物料单重", businessType = BusinessType.UPDATE)
|
||||
@SaCheckPermission("system:route:updateDanzhong")
|
||||
@PostMapping(value = "/updateDanzhong", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<Void> updateDanzhong(@RequestParam("file") MultipartFile file) throws Exception {
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
log.info("读取文件名: " + originalFilename);
|
||||
ExcelResult<JdDanZhong> result = ExcelUtil.importExcelSheet1(file.getInputStream(), JdDanZhong.class, true);
|
||||
List<JdDanZhong> list = result.getList();
|
||||
@PostMapping("/updateDanzhong")
|
||||
public R<Void> updateDanzhong() {
|
||||
final int pageSize = 500;
|
||||
int pageNum = 1;
|
||||
long totalRows = 0;
|
||||
long skippedDisabled = 0;
|
||||
long skippedNoWeight = 0;
|
||||
long submitted = 0;
|
||||
int failedTasks = 0;
|
||||
|
||||
// 创建固定大小的线程池
|
||||
ExecutorService executor = Executors.newFixedThreadPool(10);
|
||||
|
||||
list.forEach(vmi -> executor.submit(() -> {
|
||||
List<JdEntryVmi> entryID = JdUtil.getEntryID2(vmi.getMaterialCode());
|
||||
entryID.forEach(jdEntryVmi -> {
|
||||
int fmaterialid = jdEntryVmi.getFMATERIALID();
|
||||
List<Future<Void>> futures = new ArrayList<>();
|
||||
|
||||
try {
|
||||
log.info("=====================>开始更新 " + vmi.getMaterialCode() + " 的单重");
|
||||
JdUtil.updateDanzhong(fmaterialid,vmi.getDanzhong() );
|
||||
while (true) {
|
||||
LambdaQueryWrapper<ImMaterial> qw = Wrappers.lambdaQuery();
|
||||
qw.isNotNull(ImMaterial::getMaterialCode);
|
||||
qw.ne(ImMaterial::getMaterialCode, "");
|
||||
qw.and(w -> w.isNull(ImMaterial::getForbidStatus).or().ne(ImMaterial::getForbidStatus, "B"));
|
||||
qw.orderByAsc(ImMaterial::getId);
|
||||
Page<ImMaterial> page = materialMapper.selectPage(new Page<>(pageNum, pageSize), qw);
|
||||
List<ImMaterial> rows = page.getRecords();
|
||||
if (rows.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
for (ImMaterial m : rows) {
|
||||
totalRows++;
|
||||
if (m == null || StringUtils.isBlank(m.getMaterialCode())) {
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isNotBlank(m.getForbidStatus()) && "B".equalsIgnoreCase(m.getForbidStatus().trim())) {
|
||||
skippedDisabled++;
|
||||
continue;
|
||||
}
|
||||
if (m.getSingleWeight() == null) {
|
||||
skippedNoWeight++;
|
||||
continue;
|
||||
}
|
||||
final String materialCode = m.getMaterialCode().trim();
|
||||
final String danzhongStr = m.getSingleWeight().stripTrailingZeros().toPlainString();
|
||||
|
||||
futures.add(executor.submit(() -> {
|
||||
List<JdEntryVmi> entryID = JdUtil.getEntryID2(materialCode);
|
||||
if (entryID == null || entryID.isEmpty()) {
|
||||
log.warn("金蝶未查到物料编码: {}", materialCode);
|
||||
return null;
|
||||
}
|
||||
for (JdEntryVmi jdEntryVmi : entryID) {
|
||||
int fmaterialid = jdEntryVmi.getFMATERIALID();
|
||||
try {
|
||||
log.info("=====================>开始更新 {} 的单重(来自 im_material)", materialCode);
|
||||
JdUtil.updateDanzhong(fmaterialid, danzhongStr);
|
||||
} catch (Exception e) {
|
||||
log.error("更新单重失败 materialCode={}, fmaterialid={}", materialCode, fmaterialid, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}));
|
||||
submitted++;
|
||||
}
|
||||
if (rows.size() < pageSize) {
|
||||
break;
|
||||
}
|
||||
pageNum++;
|
||||
}
|
||||
|
||||
// 关闭线程池并等待所有任务完成
|
||||
for (Future<Void> future : futures) {
|
||||
try {
|
||||
future.get();
|
||||
} catch (ExecutionException e) {
|
||||
failedTasks++;
|
||||
log.error("更新物料单重任务异常: {}", e.getCause() != null ? e.getCause().getMessage() : e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("更新物料单重被中断", e);
|
||||
return R.fail("任务被中断");
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
try {
|
||||
if (!executor.awaitTermination(60, TimeUnit.MINUTES)) {
|
||||
executor.shutdownNow(); // 超时后强制关闭
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt(); // 保留中断状态
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("updateDanzhong 完成: 扫描行={}, 提交金蝶任务={}, 跳过禁用={}, 跳过无单重={}, 失败任务={}", totalRows, submitted, skippedDisabled, skippedNoWeight, failedTasks);
|
||||
if (failedTasks > 0) {
|
||||
return R.fail(String.format("部分失败: 失败任务数=%d,详见日志", failedTasks));
|
||||
}
|
||||
return R.ok("更新成功");
|
||||
}
|
||||
|
||||
@ -542,8 +606,6 @@ public class EleMaterialsController extends BaseController {
|
||||
log.info("读取文件名: " + originalFilename);
|
||||
ExcelResult<JdDanZhong> result = ExcelUtil.importExcelSheet1(file.getInputStream(), JdDanZhong.class, true);
|
||||
List<JdDanZhong> list = result.getList();
|
||||
|
||||
|
||||
return R.ok("更新成功");
|
||||
}
|
||||
|
||||
|
||||
@ -21,17 +21,26 @@ import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.excel.ExcelResult;
|
||||
import com.ruoyi.common.utils.JdUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.system.domain.ImMaterial;
|
||||
import com.ruoyi.system.domain.bo.ImMaterialBo;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.BuildCapacityDTO;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.CalcInfo;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.MaterialItem;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.StockInfo;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialsDTO;
|
||||
import com.ruoyi.system.domain.vo.BuildCapacityMaterialExportVo;
|
||||
import com.ruoyi.system.domain.vo.ImMaterialVo;
|
||||
import com.ruoyi.system.mapper.ImMaterialMapper;
|
||||
import com.ruoyi.system.service.IImMaterialService;
|
||||
import com.ruoyi.system.service.IPartCostService;
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@ -43,8 +52,15 @@ import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static com.ruoyi.common.core.mapper.BaseMapperPlus.log;
|
||||
|
||||
@ -72,6 +88,12 @@ public class ImMaterialController extends BaseController {
|
||||
private final ImMaterialMapper imMaterialMapper;
|
||||
|
||||
private final IPartCostService iPartCostService;
|
||||
|
||||
@Value("${plm.pdf.base-dir:D:/plm/pdf}")
|
||||
private String plmPdfBaseDir;
|
||||
|
||||
@Value("${plm.pdf.separator:}")
|
||||
private String plmPdfSeparator;
|
||||
/**
|
||||
* 金蝶物料保存 → 自动推送到此接口
|
||||
* SaIgnore 注解用于开放此接口,无需 token 验证
|
||||
@ -114,6 +136,426 @@ public class ImMaterialController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@PostMapping(value = "/plm/pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<Map<String, String>> receivePlmPdfDrawing(
|
||||
@RequestParam("productionOrderNo") String productionOrderNo,
|
||||
@RequestParam("materialCode") String materialCode,
|
||||
@RequestParam("materialName") String materialName,
|
||||
@RequestParam(value = "pdfFileName", required = false) String pdfFileName,
|
||||
@RequestPart("file") MultipartFile file
|
||||
) {
|
||||
if (StringUtils.isBlank(productionOrderNo)) {
|
||||
return R.fail("生产令号不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(materialCode)) {
|
||||
return R.fail("物料编码不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(materialName)) {
|
||||
return R.fail("物料名称不能为空");
|
||||
}
|
||||
if (file == null || file.isEmpty()) {
|
||||
return R.fail("PDF文件不能为空");
|
||||
}
|
||||
|
||||
String originalName = file.getOriginalFilename();
|
||||
String finalPdfFileName = StringUtils.isNotBlank(pdfFileName) ? pdfFileName : originalName;
|
||||
if (!isPdfFileName(finalPdfFileName)) {
|
||||
return R.fail("仅支持上传PDF文件");
|
||||
}
|
||||
|
||||
String safeProductionOrderNo = sanitizePathPart(productionOrderNo);
|
||||
String safeMaterialCode = sanitizeFileNamePart(materialCode);
|
||||
String safeMaterialName = sanitizeFileNamePart(materialName);
|
||||
String separator = StringUtils.trimToEmpty(plmPdfSeparator);
|
||||
|
||||
String targetBaseName = safeMaterialCode + separator + safeMaterialName;
|
||||
Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo);
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
Path targetFile = resolveAvailablePdfPath(dir, targetBaseName);
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
Map<String, String> resp = new HashMap<>(8);
|
||||
resp.put("productionOrderNo", productionOrderNo);
|
||||
resp.put("materialCode", materialCode);
|
||||
resp.put("materialName", materialName);
|
||||
resp.put("pdfFileName", finalPdfFileName);
|
||||
resp.put("savedFileName", targetFile.getFileName().toString());
|
||||
resp.put("savedDir", dir.toString());
|
||||
resp.put("savedPath", targetFile.toString());
|
||||
return R.ok(resp);
|
||||
} catch (Exception e) {
|
||||
log.error("接收PLM PDF图纸失败: productionOrderNo={}, materialCode={}", productionOrderNo, materialCode, e);
|
||||
return R.fail("接收PLM PDF图纸失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@PostMapping(value = "/plm/pdf/batch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<Map<String, Object>> receivePlmPdfDrawingsBatch(
|
||||
@RequestParam("productionOrderNo") String productionOrderNo,
|
||||
@RequestPart("items") String itemsJson,
|
||||
@RequestPart("files") MultipartFile[] files
|
||||
) {
|
||||
if (StringUtils.isBlank(productionOrderNo)) {
|
||||
return R.fail("生产令号不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(itemsJson)) {
|
||||
return R.fail("items不能为空");
|
||||
}
|
||||
if (files == null || files.length == 0) {
|
||||
return R.fail("files不能为空");
|
||||
}
|
||||
|
||||
List<PlmPdfBatchItem> items;
|
||||
try {
|
||||
items = new ObjectMapper().readValue(itemsJson, new TypeReference<List<PlmPdfBatchItem>>() {});
|
||||
} catch (JsonProcessingException e) {
|
||||
return R.fail("items不是合法JSON: " + e.getOriginalMessage());
|
||||
}
|
||||
if (items == null || items.isEmpty()) {
|
||||
return R.fail("items不能为空");
|
||||
}
|
||||
|
||||
Map<String, PlmPdfBatchItem> metaByFileName = new HashMap<>(items.size() * 2);
|
||||
for (PlmPdfBatchItem item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String pdfName = normalizeOriginalName(item.getPdfFileName());
|
||||
if (StringUtils.isBlank(pdfName)) {
|
||||
return R.fail("items中pdfFileName不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(item.getMaterialCode())) {
|
||||
return R.fail("items中materialCode不能为空: " + pdfName);
|
||||
}
|
||||
if (StringUtils.isBlank(item.getMaterialName())) {
|
||||
return R.fail("items中materialName不能为空: " + pdfName);
|
||||
}
|
||||
if (!isPdfFileName(pdfName)) {
|
||||
return R.fail("items中仅支持PDF文件名: " + pdfName);
|
||||
}
|
||||
if (metaByFileName.containsKey(pdfName)) {
|
||||
return R.fail("items中存在重复pdfFileName: " + pdfName);
|
||||
}
|
||||
metaByFileName.put(pdfName, item);
|
||||
}
|
||||
|
||||
Set<String> fileNames = new HashSet<>(files.length * 2);
|
||||
List<String> duplicateFiles = new ArrayList<>();
|
||||
List<String> filesWithoutMeta = new ArrayList<>();
|
||||
for (MultipartFile f : files) {
|
||||
if (f == null || f.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String fn = normalizeOriginalName(f.getOriginalFilename());
|
||||
if (StringUtils.isBlank(fn)) {
|
||||
return R.fail("files中存在无文件名的上传项");
|
||||
}
|
||||
if (!isPdfFileName(fn)) {
|
||||
return R.fail("仅支持上传PDF文件: " + fn);
|
||||
}
|
||||
if (!fileNames.add(fn)) {
|
||||
duplicateFiles.add(fn);
|
||||
}
|
||||
if (!metaByFileName.containsKey(fn)) {
|
||||
filesWithoutMeta.add(fn);
|
||||
}
|
||||
}
|
||||
if (!duplicateFiles.isEmpty()) {
|
||||
return R.fail("files中存在重复文件名: " + String.join(", ", duplicateFiles));
|
||||
}
|
||||
if (!filesWithoutMeta.isEmpty()) {
|
||||
return R.fail("以下文件未在items中声明: " + String.join(", ", filesWithoutMeta));
|
||||
}
|
||||
|
||||
List<String> missingFiles = new ArrayList<>();
|
||||
for (String metaFileName : metaByFileName.keySet()) {
|
||||
if (!fileNames.contains(metaFileName)) {
|
||||
missingFiles.add(metaFileName);
|
||||
}
|
||||
}
|
||||
if (!missingFiles.isEmpty()) {
|
||||
return R.fail("以下items声明的文件未上传: " + String.join(", ", missingFiles));
|
||||
}
|
||||
|
||||
String safeProductionOrderNo = sanitizePathPart(productionOrderNo);
|
||||
String separator = StringUtils.trimToEmpty(plmPdfSeparator);
|
||||
Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo);
|
||||
|
||||
List<Map<String, String>> saved = new ArrayList<>(files.length);
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
for (MultipartFile f : files) {
|
||||
if (f == null || f.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String fn = normalizeOriginalName(f.getOriginalFilename());
|
||||
PlmPdfBatchItem item = metaByFileName.get(fn);
|
||||
String safeMaterialCode = sanitizeFileNamePart(item.getMaterialCode());
|
||||
String safeMaterialName = sanitizeFileNamePart(item.getMaterialName());
|
||||
String targetBaseName = safeMaterialCode + separator + safeMaterialName;
|
||||
Path targetFile = resolveAvailablePdfPath(dir, targetBaseName);
|
||||
try (InputStream in = f.getInputStream()) {
|
||||
Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
Map<String, String> row = new HashMap<>(6);
|
||||
row.put("pdfFileName", fn);
|
||||
row.put("materialCode", item.getMaterialCode());
|
||||
row.put("materialName", item.getMaterialName());
|
||||
row.put("savedFileName", targetFile.getFileName().toString());
|
||||
row.put("savedDir", dir.toString());
|
||||
row.put("savedPath", targetFile.toString());
|
||||
saved.add(row);
|
||||
}
|
||||
|
||||
Map<String, Object> resp = new HashMap<>(6);
|
||||
resp.put("productionOrderNo", productionOrderNo);
|
||||
resp.put("savedDir", dir.toString());
|
||||
resp.put("count", saved.size());
|
||||
resp.put("items", saved);
|
||||
return R.ok(resp);
|
||||
} catch (Exception e) {
|
||||
log.error("批量接收PLM PDF图纸失败: productionOrderNo={}", productionOrderNo, e);
|
||||
return R.fail("批量接收PLM PDF图纸失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@PostMapping(value = "/plm/pdf/zip", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<Map<String, Object>> receivePlmPdfDrawingsZip(
|
||||
@RequestPart("manifest") String manifestJson,
|
||||
@RequestPart("zip") MultipartFile zipFile
|
||||
) {
|
||||
if (StringUtils.isBlank(manifestJson)) {
|
||||
return R.fail("manifest不能为空");
|
||||
}
|
||||
if (zipFile == null || zipFile.isEmpty()) {
|
||||
return R.fail("zip不能为空");
|
||||
}
|
||||
|
||||
PlmPdfManifest manifest;
|
||||
try {
|
||||
manifest = new ObjectMapper().readValue(manifestJson, PlmPdfManifest.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
return R.fail("manifest不是合法JSON: " + e.getOriginalMessage());
|
||||
}
|
||||
if (manifest == null || StringUtils.isBlank(manifest.getProductionOrderNo())) {
|
||||
return R.fail("manifest.productionOrderNo不能为空");
|
||||
}
|
||||
if (manifest.getItems() == null || manifest.getItems().isEmpty()) {
|
||||
return R.fail("manifest.items不能为空");
|
||||
}
|
||||
|
||||
String productionOrderNo = manifest.getProductionOrderNo();
|
||||
String safeProductionOrderNo = sanitizePathPart(productionOrderNo);
|
||||
String separator = StringUtils.trimToEmpty(plmPdfSeparator);
|
||||
Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo);
|
||||
|
||||
Map<String, PlmPdfManifestItem> itemByPdfPath = new LinkedHashMap<>(manifest.getItems().size() * 2);
|
||||
for (PlmPdfManifestItem item : manifest.getItems()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String pdfPath = normalizeZipEntryName(item.getPdfPath());
|
||||
if (StringUtils.isBlank(pdfPath)) {
|
||||
return R.fail("manifest.items[].pdfPath不能为空");
|
||||
}
|
||||
if (!isPdfFileName(pdfPath)) {
|
||||
return R.fail("manifest.items[].pdfPath仅支持PDF: " + pdfPath);
|
||||
}
|
||||
if (StringUtils.isBlank(item.getMaterialCode())) {
|
||||
return R.fail("manifest.items[].materialCode不能为空: " + pdfPath);
|
||||
}
|
||||
if (StringUtils.isBlank(item.getMaterialName())) {
|
||||
return R.fail("manifest.items[].materialName不能为空: " + pdfPath);
|
||||
}
|
||||
if (itemByPdfPath.containsKey(pdfPath)) {
|
||||
return R.fail("manifest中存在重复pdfPath: " + pdfPath);
|
||||
}
|
||||
itemByPdfPath.put(pdfPath, item);
|
||||
}
|
||||
|
||||
Path tempZip = null;
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
tempZip = Files.createTempFile("plm-pdf-", ".zip");
|
||||
try (InputStream in = zipFile.getInputStream()) {
|
||||
Files.copy(in, tempZip, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
Map<String, ZipEntry> pdfEntryByPath = new HashMap<>(itemByPdfPath.size() * 2);
|
||||
Set<String> extraPdfFiles = new LinkedHashSet<>();
|
||||
try (ZipFile zf = new ZipFile(tempZip.toFile())) {
|
||||
Enumeration<? extends ZipEntry> entries = zf.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry ze = entries.nextElement();
|
||||
if (ze == null || ze.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
String name = normalizeZipEntryName(ze.getName());
|
||||
if (StringUtils.isBlank(name)) {
|
||||
continue;
|
||||
}
|
||||
if (!isPdfFileName(name)) {
|
||||
continue;
|
||||
}
|
||||
if (pdfEntryByPath.containsKey(name)) {
|
||||
return R.fail("zip中存在重复文件路径: " + name);
|
||||
}
|
||||
pdfEntryByPath.put(name, ze);
|
||||
if (!itemByPdfPath.containsKey(name)) {
|
||||
extraPdfFiles.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> missing = new ArrayList<>();
|
||||
for (String pdfPath : itemByPdfPath.keySet()) {
|
||||
if (!pdfEntryByPath.containsKey(pdfPath)) {
|
||||
missing.add(pdfPath);
|
||||
}
|
||||
}
|
||||
if (!missing.isEmpty()) {
|
||||
return R.fail("zip缺少以下PDF文件: " + String.join(", ", missing));
|
||||
}
|
||||
if (!extraPdfFiles.isEmpty()) {
|
||||
return R.fail("zip包含未在manifest声明的PDF文件: " + String.join(", ", extraPdfFiles));
|
||||
}
|
||||
|
||||
List<Map<String, String>> saved = new ArrayList<>(itemByPdfPath.size());
|
||||
for (Map.Entry<String, PlmPdfManifestItem> kv : itemByPdfPath.entrySet()) {
|
||||
String pdfPath = kv.getKey();
|
||||
PlmPdfManifestItem item = kv.getValue();
|
||||
ZipEntry ze = pdfEntryByPath.get(pdfPath);
|
||||
String safeMaterialCode = sanitizeFileNamePart(item.getMaterialCode());
|
||||
String safeMaterialName = sanitizeFileNamePart(item.getMaterialName());
|
||||
String targetBaseName = safeMaterialCode + separator + safeMaterialName;
|
||||
Path targetFile = resolveAvailablePdfPath(dir, targetBaseName);
|
||||
try (InputStream in = zf.getInputStream(ze)) {
|
||||
Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
Map<String, String> row = new HashMap<>(7);
|
||||
row.put("pdfPath", pdfPath);
|
||||
row.put("materialCode", item.getMaterialCode());
|
||||
row.put("materialName", item.getMaterialName());
|
||||
row.put("savedFileName", targetFile.getFileName().toString());
|
||||
row.put("savedDir", dir.toString());
|
||||
row.put("savedPath", targetFile.toString());
|
||||
saved.add(row);
|
||||
}
|
||||
|
||||
Map<String, Object> resp = new HashMap<>(6);
|
||||
resp.put("productionOrderNo", productionOrderNo);
|
||||
resp.put("savedDir", dir.toString());
|
||||
resp.put("count", saved.size());
|
||||
resp.put("items", saved);
|
||||
return R.ok(resp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("接收PLM PDF压缩包失败: productionOrderNo={}", manifest.getProductionOrderNo(), e);
|
||||
return R.fail("接收PLM PDF压缩包失败: " + e.getMessage());
|
||||
} finally {
|
||||
if (tempZip != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempZip);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPdfFileName(String fileName) {
|
||||
if (StringUtils.isBlank(fileName)) {
|
||||
return false;
|
||||
}
|
||||
return fileName.trim().toLowerCase(Locale.ROOT).endsWith(".pdf");
|
||||
}
|
||||
|
||||
private static String normalizeOriginalName(String fileName) {
|
||||
String s = StringUtils.trimToEmpty(fileName);
|
||||
if (StringUtils.isBlank(s)) {
|
||||
return s;
|
||||
}
|
||||
s = s.replace("\\", "/");
|
||||
int idx = s.lastIndexOf('/');
|
||||
if (idx >= 0 && idx + 1 < s.length()) {
|
||||
s = s.substring(idx + 1);
|
||||
}
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
private static String normalizeZipEntryName(String entryName) {
|
||||
String s = StringUtils.trimToEmpty(entryName);
|
||||
if (StringUtils.isBlank(s)) {
|
||||
return s;
|
||||
}
|
||||
s = s.replace("\\", "/");
|
||||
while (s.startsWith("/")) {
|
||||
s = s.substring(1);
|
||||
}
|
||||
while (s.startsWith("./")) {
|
||||
s = s.substring(2);
|
||||
}
|
||||
if (s.contains("..")) {
|
||||
throw new IllegalArgumentException("非法zip路径: " + entryName);
|
||||
}
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
private static String sanitizePathPart(String input) {
|
||||
String s = StringUtils.trimToEmpty(input);
|
||||
s = s.replace("\\", "_").replace("/", "_");
|
||||
return sanitizeFileNamePart(s);
|
||||
}
|
||||
|
||||
private static String sanitizeFileNamePart(String input) {
|
||||
String s = StringUtils.trimToEmpty(input);
|
||||
s = s.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
s = s.replaceAll("\\s+", " ");
|
||||
s = s.trim();
|
||||
if (s.length() > 120) {
|
||||
s = s.substring(0, 120);
|
||||
}
|
||||
if (StringUtils.isBlank(s)) {
|
||||
return "_";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static Path resolveAvailablePdfPath(Path dir, String targetBaseName) {
|
||||
String base = sanitizeFileNamePart(targetBaseName);
|
||||
Path p = dir.resolve(base + ".pdf");
|
||||
if (!Files.exists(p)) {
|
||||
return p;
|
||||
}
|
||||
String suffix = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
|
||||
return dir.resolve(base + "-" + suffix + ".pdf");
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PlmPdfBatchItem {
|
||||
private String pdfFileName;
|
||||
private String materialCode;
|
||||
private String materialName;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PlmPdfManifest {
|
||||
private String productionOrderNo;
|
||||
private List<PlmPdfManifestItem> items;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PlmPdfManifestItem {
|
||||
private String pdfPath;
|
||||
private String materialCode;
|
||||
private String materialName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询物料检索列表
|
||||
*/
|
||||
@ -186,6 +628,16 @@ public class ImMaterialController extends BaseController {
|
||||
return toAjax(iImMaterialService.deleteWithValidByIds(Arrays.asList(ids), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发:全量分页刷新 im_material 单重(按物料编码解析);仅单重变化时更新 updateTime、modify_date
|
||||
*/
|
||||
@SaCheckPermission("system:material:edit")
|
||||
@Log(title = "刷新物料单重", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/refreshSingleWeightFromOrders")
|
||||
public R<Map<String, Object>> refreshSingleWeightFromOrders() {
|
||||
return iImMaterialService.refreshImMaterialSingleWeightFromOrders();
|
||||
}
|
||||
|
||||
@PostMapping("/importTemplate")
|
||||
public void importTemplate(HttpServletResponse response) {
|
||||
ExcelUtil.exportExcel(new ArrayList<>(), "用户数据", ImMaterialBo.class, response);
|
||||
@ -485,6 +937,91 @@ public class ImMaterialController extends BaseController {
|
||||
public TableDataInfo search(@RequestParam(value = "query", required = false) String query) {
|
||||
return iImMaterialService.search(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 齐套物料查询
|
||||
*/
|
||||
@SaCheckPermission("system:material:buildCapacity")
|
||||
@Log(title = "齐套物料", businessType = BusinessType.EXPORT)
|
||||
@GetMapping("/buildCapacity")
|
||||
public R<BuildCapacityDTO> buildCapacity(@RequestParam("productionOrderNo") String productionOrderNo,
|
||||
@RequestParam("productCode") String productCode,
|
||||
@RequestParam(value = "productName", required = false) String productName) {
|
||||
return R.ok(iImMaterialService.queryBuildCapacity(productionOrderNo, productCode, productName));
|
||||
}
|
||||
|
||||
@SaCheckPermission("system:material:exportBuildCapacity")
|
||||
@Log(title = "齐套物料导出", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/buildCapacity/export")
|
||||
public void exportBuildCapacity(@RequestParam("productionOrderNo") String productionOrderNo,
|
||||
@RequestParam("productCode") String productCode,
|
||||
@RequestParam(value = "productName", required = false) String productName,
|
||||
HttpServletResponse response) {
|
||||
BuildCapacityDTO dto = iImMaterialService.queryBuildCapacity(productionOrderNo, productCode, productName);
|
||||
List<BuildCapacityMaterialExportVo> rows = new ArrayList<>();
|
||||
if (dto != null) {
|
||||
appendBuildCapacityRows(rows, productionOrderNo, dto, "自制", dto.getSelfMadeMaterials());
|
||||
appendBuildCapacityRows(rows, productionOrderNo, dto, "采购", dto.getPurchaseMaterials());
|
||||
}
|
||||
ExcelUtil.exportExcel(rows, "齐套物料", BuildCapacityMaterialExportVo.class, response);
|
||||
}
|
||||
|
||||
@GetMapping("/buildCapacityByProductionOrderNo")
|
||||
public R<List<BuildCapacityDTO>> buildCapacityByProductionOrderNo(@RequestParam("productionOrderNo") String productionOrderNo) {
|
||||
return R.ok(iImMaterialService.queryBuildCapacityByProductionOrderNo(productionOrderNo));
|
||||
}
|
||||
|
||||
@GetMapping("/productionOrderMaterials")
|
||||
public R<ProductionOrderMaterialsDTO> productionOrderMaterials(@RequestParam("productionOrderNo") String productionOrderNo,
|
||||
@RequestParam("productCode") String productCode) {
|
||||
return R.ok(iImMaterialService.queryProductionOrderMaterials(productionOrderNo, productCode));
|
||||
}
|
||||
|
||||
private static void appendBuildCapacityRows(List<BuildCapacityMaterialExportVo> out,
|
||||
String productionOrderNo,
|
||||
BuildCapacityDTO dto,
|
||||
String materialType,
|
||||
List<MaterialItem> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (MaterialItem item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
BuildCapacityMaterialExportVo row = new BuildCapacityMaterialExportVo();
|
||||
row.setProductionOrderNo(productionOrderNo);
|
||||
row.setProductCode(dto.getProductCode());
|
||||
row.setProductName(dto.getProductName());
|
||||
row.setMaxBuildQty(dto.getMaxBuildQty());
|
||||
row.setMaterialType(materialType);
|
||||
row.setMaterialCode(item.getMaterialCode());
|
||||
row.setMaterialName(item.getMaterialName());
|
||||
row.setRequiredQty(item.getRequiredQty());
|
||||
row.setMaterial(item.getMaterial());
|
||||
row.setSingleWeight(item.getSingleWeight());
|
||||
row.setTotalWeight(item.getTotalWeight());
|
||||
row.setRemark(item.getRemark());
|
||||
row.setBatchQuantity(item.getBatchQuantity());
|
||||
|
||||
StockInfo stock = item.getStock();
|
||||
if (stock != null) {
|
||||
row.setOnHandQty(stock.getOnHandQty());
|
||||
row.setPoNotInQty(stock.getPoNotInQty());
|
||||
row.setMoNotInQty(stock.getMoNotInQty());
|
||||
row.setMoNotPickQty(stock.getMoNotPickQty());
|
||||
}
|
||||
CalcInfo calc = item.getCalc();
|
||||
if (calc != null) {
|
||||
row.setAvailableQty(calc.getAvailableQty());
|
||||
row.setFinalAvailableQty(calc.getFinalAvailableQty());
|
||||
row.setCanSupportQty(calc.getCanSupportQty());
|
||||
}
|
||||
row.setStatus(item.getStatus() != null ? item.getStatus().name() : null);
|
||||
out.add(row);
|
||||
}
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@PostMapping("/getObtainPartData2")
|
||||
public R<Void> getObtainPartData() {
|
||||
|
||||
@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.ruoyi.common.excel.DefaultExcelListener;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.FtpUtil;
|
||||
import com.ruoyi.common.utils.HttpUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.VersionComparator;
|
||||
import com.ruoyi.common.utils.file.SmbUtil;
|
||||
@ -32,6 +33,7 @@ import com.ruoyi.system.mapper.ProcessOrderProMapper;
|
||||
import com.ruoyi.system.mapper.SafetyStockMapper;
|
||||
import com.ruoyi.system.runner.JdUtil;
|
||||
import com.ruoyi.system.service.*;
|
||||
import com.ruoyi.system.utils.ProductionOrderNoUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCell;
|
||||
import org.apache.poi.xssf.usermodel.XSSFRow;
|
||||
@ -40,6 +42,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -244,7 +247,14 @@ public class ProcessOrderProController extends BaseController {
|
||||
String s = iProcessOrderProService.executDrawing(orderPro);
|
||||
return R.ok(s);
|
||||
}
|
||||
|
||||
@GetMapping("/getstate")
|
||||
public R<JSONObject> getPlotState() {
|
||||
String result = HttpUtils.sendGet("http://192.168.5.18:9000/getstate", "UTF-8");
|
||||
if (StringUtils.isBlank(result)) {
|
||||
return R.ok("操作成功", null);
|
||||
}
|
||||
return R.ok("操作成功", JSON.parseObject(result));
|
||||
}
|
||||
@GetMapping("/executDrawing/progress/{id}")
|
||||
public SseEmitter executDrawingProgress(@PathVariable Long id) {
|
||||
return iProcessOrderProService.startDrawingSse(id);
|
||||
@ -933,8 +943,8 @@ public class ProcessOrderProController extends BaseController {
|
||||
vo.setBatchQuantity(getCellValueAsString(row.getCell(8))); // 批次数量
|
||||
vo.setParentPart(getCellValueAsString(row.getCell(9))); // 上级部件
|
||||
vo.setParentDrawingNo(getCellValueAsString(row.getCell(10))); // 上级部件图号
|
||||
vo.setMainProducts(getCellValueAsString(row.getCell(11))); // 主产品图号
|
||||
vo.setMainProductsName(getCellValueAsString(row.getCell(12))); // 主产品名称
|
||||
vo.setMainPart(getCellValueAsString(row.getCell(11))); // 主产品图号
|
||||
vo.setMainName(getCellValueAsString(row.getCell(12))); // 主产品名称
|
||||
|
||||
resultList.add(vo);
|
||||
}
|
||||
@ -1095,8 +1105,8 @@ public class ProcessOrderProController extends BaseController {
|
||||
}
|
||||
map.put("batchQuantityInt", batchQuantityInt);
|
||||
|
||||
map.put("mainProducts", item.getMainProducts());
|
||||
map.put("mainProductsName", item.getMainProductsName());
|
||||
map.put("mainProducts", item.getMainPart());
|
||||
map.put("mainProductsName", item.getMainName());
|
||||
mapList.add(map);
|
||||
}
|
||||
return mapList;
|
||||
@ -1882,6 +1892,25 @@ public class ProcessOrderProController extends BaseController {
|
||||
return mapList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出图完成后由前端触发:先推送「出图完成」Markdown,再生成工艺计划表 Excel 并发至钉钉 notifyUserId。
|
||||
*/
|
||||
@SaCheckPermission("system:orderPro:sendProcessRoutePlanExcel")
|
||||
@Log(title = "发送工艺计划表", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/sendProcessRoutePlanExcel/{id}")
|
||||
public R<Void> sendProcessRoutePlanExcel(@NotNull(message = "项目ID不能为空") @PathVariable Long id) {
|
||||
return iProcessOrderProService.sendProcessRoutePlanExcel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工艺计划表钉钉发送成功次数(用于按钮文案)
|
||||
*/
|
||||
@SaIgnore
|
||||
@GetMapping("/routePlanSendCount/{id}")
|
||||
public R<Integer> getRoutePlanSendCount(@NotNull(message = "项目ID不能为空") @PathVariable Long id) {
|
||||
return iProcessOrderProService.getRoutePlanSendCount(id);
|
||||
}
|
||||
|
||||
@SaCheckPermission("system:orderPro:exportRoute3")
|
||||
@Log(title = "下载工艺生产表3", businessType = BusinessType.EXPORT)
|
||||
@GetMapping ("/exportRoute3")
|
||||
@ -1925,30 +1954,28 @@ public class ProcessOrderProController extends BaseController {
|
||||
return generateRoute3Excel(id, false);
|
||||
}
|
||||
|
||||
/** 去掉 Windows 文件名非法字符,避免生成或钉钉展示异常 */
|
||||
private static String sanitizeFileNameSegment(String name) {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return "unknown";
|
||||
}
|
||||
String trimmed = name.trim();
|
||||
String illegal = "\\/:*?\"<>|";
|
||||
StringBuilder sb = new StringBuilder(trimmed.length());
|
||||
for (int i = 0; i < trimmed.length(); i++) {
|
||||
char c = trimmed.charAt(i);
|
||||
sb.append(illegal.indexOf(c) >= 0 ? '_' : c);
|
||||
}
|
||||
String out = sb.toString();
|
||||
return out.isEmpty() ? "unknown" : out;
|
||||
}
|
||||
|
||||
public String generateRoute3Excel(Long id, boolean isDrawing) throws Exception {
|
||||
ProcessOrderPro orderPro = processOrderProMapper.selectById(id);
|
||||
// 下载Excel文件
|
||||
// 1. 解析年份
|
||||
String productionCode = orderPro.getProductionOrderNo();
|
||||
String year = "2025"; // 默认
|
||||
if (productionCode != null) {
|
||||
if (productionCode.startsWith("EY")) {
|
||||
if (productionCode.length() >= 4) {
|
||||
try {
|
||||
Integer.parseInt(productionCode.substring(2, 4));
|
||||
year = "20" + productionCode.substring(2, 4);
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
} else {
|
||||
int firstDash = productionCode.indexOf("-");
|
||||
if (firstDash >= 0 && firstDash + 3 <= productionCode.length()) {
|
||||
try {
|
||||
Integer.parseInt(productionCode.substring(firstDash + 1, firstDash + 3));
|
||||
year = "20" + productionCode.substring(firstDash + 1, firstDash + 3);
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
String year = ProductionOrderNoUtils.extractYear(productionCode);
|
||||
|
||||
// 2. 构建FTP路径和本地路径
|
||||
String ftpPath = "/" + year + "/" + productionCode;
|
||||
@ -2313,7 +2340,11 @@ public class ProcessOrderProController extends BaseController {
|
||||
|
||||
// 使用Excel模板文件
|
||||
String templatePath = "jpg/生产及工艺计划模版.xlsx";
|
||||
String outputPath = "D:/file/" + orderPro.getProductionOrderNo() + "生产及工艺计划表.xlsx";
|
||||
String safeOrderNo = sanitizeFileNameSegment(orderPro.getProductionOrderNo());
|
||||
// 钉钉发送场景用「令号+工艺表」,避免过长中文后缀在客户端显示异常;浏览器导出仍为「生产及工艺计划表」
|
||||
String outputPath = isDrawing
|
||||
? ("D:/file/" + safeOrderNo + "工艺表.xlsx")
|
||||
: ("D:/file/" + safeOrderNo + "生产及工艺计划表.xlsx");
|
||||
|
||||
// 准备模板数据
|
||||
Map<String, Object> staticDataMap = new HashMap<>();
|
||||
@ -2393,7 +2424,7 @@ public class ProcessOrderProController extends BaseController {
|
||||
return;
|
||||
}
|
||||
// 检查 quantity 字段(假设你的数量字段名是 quantity 或者 unitQuantity 等,这里假设是 getQuantity(),如果不对请调整为对应的 get 方法)
|
||||
if (orderPro.getQuantity() != null && orderPro.getQuantity().intValue() == 0) {
|
||||
if (orderPro.getIsDownload() != null && orderPro.getIsDownload().intValue() == 0) {
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
response.getWriter().write(JSONObject.toJSONString(R.fail("当前图纸数量不允许下载")));
|
||||
return;
|
||||
|
||||
@ -40,6 +40,8 @@ import com.ruoyi.system.mapper.ProcessOrderProMapper;
|
||||
import com.ruoyi.system.mapper.ProcessRouteMapper;
|
||||
import com.ruoyi.system.runner.JdUtil;
|
||||
import com.ruoyi.system.service.*;
|
||||
import com.ruoyi.system.service.support.OperationPlanningWorkshopUpdateService;
|
||||
import com.ruoyi.system.service.support.OperationReportWorkshopUpdateService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -62,6 +64,8 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* 工艺路线
|
||||
@ -76,6 +80,8 @@ import java.util.stream.Collectors;
|
||||
public class ProcessRouteController extends BaseController {
|
||||
private final ProcessRouteMapper baseMapper;
|
||||
private final IProcessRouteService iProcessRouteService;
|
||||
private final OperationPlanningWorkshopUpdateService operationPlanningWorkshopUpdateService;
|
||||
private final OperationReportWorkshopUpdateService operationReportWorkshopUpdateService;
|
||||
private final IBomDetailsService iBomDetailsService;
|
||||
private final ProcessOrderProMapper proMapper;
|
||||
private final IImMaterialService materialService;
|
||||
@ -603,6 +609,15 @@ public class ProcessRouteController extends BaseController {
|
||||
if (!zipFile.exists()) {
|
||||
throw new FileNotFoundException("ZIP 文件未找到: " + zipFilePath);
|
||||
}
|
||||
if (zipFile.length() <= 0) {
|
||||
throw new IOException("ZIP 文件为空: " + zipFilePath);
|
||||
}
|
||||
try (ZipFile zf = new ZipFile(zipFile)) {
|
||||
Enumeration<? extends ZipEntry> entries = zf.entries();
|
||||
if (entries == null || !entries.hasMoreElements()) {
|
||||
throw new IOException("ZIP 文件中没有PDF: " + zipFilePath);
|
||||
}
|
||||
}
|
||||
FileInputStream fis = new FileInputStream(zipFile);
|
||||
OutputStream outputStreams = response.getOutputStream();
|
||||
response.setContentType("application/zip");
|
||||
@ -624,6 +639,46 @@ public class ProcessRouteController extends BaseController {
|
||||
return iProcessRouteService.updateProcessPlan(rooteProdet);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按车间+物料默认条件拉取工序计划,回填工艺时间,保存时部门统一为 018(与 {@link OperationPlanningWorkshopUpdateService} 一致)。
|
||||
*/
|
||||
@Log(title = "按车间物料更新工序计划(默认条件)")
|
||||
@SaCheckPermission("system:route:updateProcessPlan")
|
||||
@PostMapping("/updateProcessPlanByWorkshop")
|
||||
public R<List<Model>> updateProcessPlanByWorkshop() throws Exception {
|
||||
return R.ok(operationPlanningWorkshopUpdateService.updateProcessPlanByWorkshopFilter());
|
||||
}
|
||||
|
||||
/**
|
||||
* 同上,BillQuery 过滤条件可自定义(部门 Compare/Value、物料 Compare/Value)。
|
||||
*/
|
||||
@Log(title = "按车间物料更新工序计划(自定义条件)")
|
||||
@SaCheckPermission("system:route:updateProcessPlan")
|
||||
@PostMapping("/updateProcessPlanByWorkshopCustom")
|
||||
public R<List<Model>> updateProcessPlanByWorkshopCustom(
|
||||
@RequestParam String departmentCompare,
|
||||
@RequestParam String departmentNumber,
|
||||
@RequestParam String productCompare,
|
||||
@RequestParam String productFilterValue) throws Exception {
|
||||
return R.ok(operationPlanningWorkshopUpdateService.updateProcessPlanByWorkshopFilter(
|
||||
departmentCompare, departmentNumber, productCompare, productFilterValue));
|
||||
}
|
||||
|
||||
@Log(title = "按车间物料更新工序汇报单(默认条件)")
|
||||
@SaCheckPermission("system:route:updateProcessPlan")
|
||||
@PostMapping("/updateOperationReportByWorkshop")
|
||||
public R<List<Model>> updateOperationReportByWorkshop() throws Exception {
|
||||
return R.ok(operationReportWorkshopUpdateService.updateOperationReportByWorkshopFilter());
|
||||
}
|
||||
|
||||
@Log(title = "按车间物料更新工序汇报单(固定条件67/007/17/-MP)")
|
||||
@SaCheckPermission("system:route:updateProcessPlan")
|
||||
@PostMapping("/updateOperationReportByWorkshopCustom")
|
||||
public R<List<Model>> updateOperationReportByWorkshopCustom() throws Exception {
|
||||
return R.ok(operationReportWorkshopUpdateService.updateOperationReportByWorkshopFilter(
|
||||
"67", "007", "17", "-MP"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新生产订单的时间
|
||||
*/
|
||||
|
||||
@ -3,6 +3,8 @@ package com.ruoyi.system.controller;
|
||||
import java.util.*;
|
||||
|
||||
import com.ruoyi.system.domain.ProductionOrder;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO;
|
||||
import com.ruoyi.system.domain.dto.PurchaseOrderExcelDTO;
|
||||
import com.ruoyi.system.domain.dto.PurchaseRequestExcelDTO;
|
||||
import com.ruoyi.system.runner.JdUtil;
|
||||
@ -138,6 +140,19 @@ public class ProductionOrderController extends BaseController {
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试方法:导入物料编码后批量匹配单重并导出
|
||||
* Excel列:物料编码、物料名称、单重
|
||||
*/
|
||||
@Log(title = "物料单重匹配导出", businessType = BusinessType.IMPORT)
|
||||
@SaCheckPermission("system:order:import")
|
||||
@PostMapping(value = "/testExportWeightByMaterialCode", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public void testExportWeightByMaterialCode(@RequestPart("file") MultipartFile file, HttpServletResponse response) throws Exception {
|
||||
List<ProductionOrderWeightImportDTO> importRows = ExcelUtil.importExcel(file.getInputStream(), ProductionOrderWeightImportDTO.class);
|
||||
List<ProductionOrderWeightExportDTO> exportRows = iProductionOrderService.matchSingleWeightByMaterialCode(importRows);
|
||||
ExcelUtil.exportExcel(exportRows, "物料单重匹配结果", ProductionOrderWeightExportDTO.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组焊件bom列表
|
||||
* @param productionOrderVo
|
||||
|
||||
@ -12,6 +12,7 @@ import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.system.domain.FigureSave;
|
||||
import com.ruoyi.system.domain.ProductionOrder;
|
||||
import com.ruoyi.system.domain.ProductionPlan;
|
||||
import com.ruoyi.system.domain.bo.ProductionPlanBo;
|
||||
import com.ruoyi.system.domain.vo.ProductionPlanVo;
|
||||
@ -20,6 +21,7 @@ import com.ruoyi.system.mapper.FigureSaveMapper;
|
||||
import com.ruoyi.system.mapper.ProductionPlanMapper;
|
||||
import com.ruoyi.system.runner.JdUtil;
|
||||
import com.ruoyi.system.service.IFigureSaveService;
|
||||
import com.ruoyi.system.service.IProductionOrderService;
|
||||
import com.ruoyi.system.service.IProductionPlanService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
@ -51,6 +53,7 @@ import java.util.List;
|
||||
public class ProductionPlanController extends BaseController {
|
||||
private final IFigureSaveService iFigureSaveService;
|
||||
private final IProductionPlanService iProductionPlanService;
|
||||
private final IProductionOrderService iProductionOrderService;
|
||||
@Resource
|
||||
private ProductionPlanMapper productionPlanMapper;
|
||||
@Resource
|
||||
@ -210,6 +213,26 @@ public class ProductionPlanController extends BaseController {
|
||||
return R.ok("状态更新成功");
|
||||
}
|
||||
|
||||
@SaCheckPermission("system:plan:productionOrderNoOptions")
|
||||
@Log(title = "查询生产令号及主产品,预配套使用", businessType = BusinessType.UPDATE)
|
||||
@GetMapping("/productionOrderNoOptions")
|
||||
public R<List<String>> productionOrderNoOptions(@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
return R.ok("ok", iProductionOrderService.selectProductionOrderNoOptions(keyword, limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据生产令号查询所有的装配bom
|
||||
*
|
||||
* @param productionOrderNo 生产令号
|
||||
*/
|
||||
@SaCheckPermission("system:plan:selectByProCode")
|
||||
@Log(title = "排产计划", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/selectByProCode")
|
||||
public R<List<ProductionOrder>> selectByProCode(@RequestParam("productionOrderNo") String productionOrderNo) {
|
||||
List<ProductionOrder> productionOrders = iProductionOrderService.selectByProCode(productionOrderNo.trim());
|
||||
return R.ok("ok", productionOrders);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -46,8 +46,7 @@ public class SfcOperationPlanningMainController extends BaseController {
|
||||
/**
|
||||
* 同步工序计划数据
|
||||
*/
|
||||
@SaIgnore
|
||||
//@SaCheckPermission("system:operationPlanningMain:sync")
|
||||
@SaCheckPermission("system:operationPlanningMain:sync")
|
||||
@Log(title = "工序计划数据同步", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/sync")
|
||||
public R<Void> syncData() {
|
||||
@ -57,7 +56,7 @@ public class SfcOperationPlanningMainController extends BaseController {
|
||||
/**
|
||||
* 直接从K3查询工序计划数据(不入库测试)
|
||||
*/
|
||||
@SaIgnore
|
||||
@SaCheckPermission("system:operationPlanningMain:queryK3")
|
||||
@GetMapping("/queryFromK3")
|
||||
public R<List<OperationPlanResultDTO>> queryFromK3() {
|
||||
return R.ok(iSfcOperationPlanningMainService.queryFromK3Directly());
|
||||
@ -66,13 +65,13 @@ public class SfcOperationPlanningMainController extends BaseController {
|
||||
/**
|
||||
* 分页查询工序计划主及明细级联数据
|
||||
*/
|
||||
@SaIgnore
|
||||
@SaCheckPermission("system:operationPlanningMain:list")
|
||||
@GetMapping("/cascadeList")
|
||||
public TableDataInfo<OperationPlanResultDTO> cascadeList(SfcOperationPlanningMainBo bo, PageQuery pageQuery) {
|
||||
return iSfcOperationPlanningMainService.queryCascadePageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@SaCheckPermission("system:operationPlanningMain:updateOperators")
|
||||
@Log(title = "工序作业人员/设备更新", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/updateOperators")
|
||||
public R<Void> updateOperators(@Validated @RequestBody OperationPlanningUpdateOperatorsReq req) {
|
||||
|
||||
@ -33,7 +33,10 @@ public class ProcessOrderPro extends BaseEntity {
|
||||
* 是否延期:0否 1是
|
||||
*/
|
||||
private Integer whetherDelay;
|
||||
|
||||
/**
|
||||
* 是否可以下载
|
||||
*/
|
||||
private Integer isDownload;
|
||||
/**
|
||||
* 延期天数
|
||||
*/
|
||||
@ -134,4 +137,9 @@ public class ProcessOrderPro extends BaseEntity {
|
||||
*/
|
||||
private String notificationCardId;
|
||||
|
||||
/**
|
||||
* 工艺计划表(钉钉)发送次数,每次发送成功 +1
|
||||
*/
|
||||
private Integer routePlanSendCount;
|
||||
|
||||
}
|
||||
|
||||
@ -66,6 +66,14 @@ public class ProductionOrder extends BaseEntity {
|
||||
* 部件图号
|
||||
*/
|
||||
private String parentDrawingNo;
|
||||
/**
|
||||
* 主产品名称
|
||||
*/
|
||||
private String mainName;
|
||||
/**
|
||||
* 主产品图号
|
||||
*/
|
||||
private String mainPart;
|
||||
/**
|
||||
* 批次数量
|
||||
*/
|
||||
|
||||
@ -42,6 +42,9 @@ public class SfcOperationPlanningMain extends BaseEntity {
|
||||
* 生产令号
|
||||
*/
|
||||
private String hbytSclh;
|
||||
|
||||
@TableField("hbyt_xmh")
|
||||
private String fHbytXmh;
|
||||
/**
|
||||
* 产品编码
|
||||
*/
|
||||
|
||||
@ -81,6 +81,10 @@ public class ProcessOrderProBo extends BaseEntity {
|
||||
*/
|
||||
private List<String> isEnterpriseStandardList;
|
||||
|
||||
/**
|
||||
* 是否可以下载
|
||||
*/
|
||||
private Integer isDownload;
|
||||
|
||||
private String drawingPath;
|
||||
/**
|
||||
|
||||
@ -64,4 +64,14 @@ public class ProductionOrderBo extends BaseEntity {
|
||||
* 批次数量
|
||||
*/
|
||||
private String batchQuantity;
|
||||
|
||||
/**
|
||||
* 主产品名称
|
||||
*/
|
||||
private String mainName;
|
||||
|
||||
/**
|
||||
* 主产品图号
|
||||
*/
|
||||
private String mainPart;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.ruoyi.system.domain.bo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
@ -50,7 +51,8 @@ public class SfcOperationPlanningMainBo extends BaseEntity {
|
||||
*/
|
||||
@NotBlank(message = "生产令号不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String hbytSclh;
|
||||
|
||||
@NotBlank(message = "项目号不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String fHbytXmh;
|
||||
/**
|
||||
* 产品编码
|
||||
*/
|
||||
|
||||
@ -8,7 +8,7 @@ import java.util.Date;
|
||||
@Data
|
||||
public class ConstructionDelayDTO {
|
||||
//生产令号
|
||||
@JsonProperty("F_HBYT_SCLH")
|
||||
@JsonProperty("F_HBYT_XMH")
|
||||
private String F_HBYT_SCLH;
|
||||
//单据编码
|
||||
@JsonProperty("FBillNo")
|
||||
|
||||
@ -18,6 +18,8 @@ public class OperationPlanResultDTO {
|
||||
*/
|
||||
private String batchNo;
|
||||
|
||||
private String projectNo;
|
||||
|
||||
/**
|
||||
* 产品编码
|
||||
*/
|
||||
|
||||
@ -25,7 +25,11 @@ public class OperationPlanningDTO {
|
||||
* 生产令号
|
||||
*/
|
||||
@JsonProperty("F_HBYT_SCLH")
|
||||
private String fHbytSclh;
|
||||
private String fHbytSclh;/**
|
||||
* 生产令号
|
||||
*/
|
||||
@JsonProperty("F_HBYT_XMH")
|
||||
private String fHbytXmh;
|
||||
|
||||
/**
|
||||
* 生产订单编号
|
||||
|
||||
@ -58,6 +58,60 @@ public class ProcessDetailDTO {
|
||||
*/
|
||||
private Double wastageQty;
|
||||
|
||||
/**
|
||||
* 综合看板:统计日(请求参数 boardReportDay,默认服务器当天)内金蝶工序汇报汇总与本工序号匹配的完工数量合计。
|
||||
*/
|
||||
private Double dayReportFinishQty;
|
||||
|
||||
/** 同上:合格数量合计 */
|
||||
private Double dayReportQuaQty;
|
||||
|
||||
/** 同上:废品/不良数量合计 */
|
||||
private Double dayReportFailQty;
|
||||
|
||||
/**
|
||||
* 综合看板:生产入库数量。当前追溯数据未按工序拆分,仅在<strong>该工序计划主档</strong>下工序号最大的一道工序上回填本生产订单入库数量合计,其余工序为 null。
|
||||
*/
|
||||
private Double operationInstockQty;
|
||||
|
||||
// ---------- 综合看板:金蝶「工序委外转移」报表并入工序明细(按 boardReportDay 切分) ----------
|
||||
|
||||
/**
|
||||
* 单据日期 < boardReportDay 的委外<strong>发出</strong>数量累计(多行合并)。
|
||||
* 仅当本工序被识别为委外/外协相关且匹配到报表行时有值,否则为 null。
|
||||
*/
|
||||
private Double subcontractLeaveQtyBeforeReportDay;
|
||||
|
||||
/**
|
||||
* 单据日期 < boardReportDay 的委外<strong>接收</strong>数量累计。
|
||||
*/
|
||||
private Double subcontractEnterQtyBeforeReportDay;
|
||||
|
||||
/**
|
||||
* 单据日期 < boardReportDay 的委外<strong>合格</strong>数量累计。
|
||||
*/
|
||||
private Double subcontractQualifiedQtyBeforeReportDay;
|
||||
|
||||
/**
|
||||
* 单据日期 = boardReportDay 当天的委外<strong>发出</strong>合计(可多笔合并)。
|
||||
*/
|
||||
private Double subcontractLeaveQtyOnReportDay;
|
||||
|
||||
/**
|
||||
* 单据日期 = boardReportDay 当天的委外<strong>接收</strong>合计。
|
||||
*/
|
||||
private Double subcontractEnterQtyOnReportDay;
|
||||
|
||||
/**
|
||||
* 单据日期 = boardReportDay 当天的委外<strong>合格</strong>合计。
|
||||
*/
|
||||
private Double subcontractQualifiedQtyOnReportDay;
|
||||
|
||||
/**
|
||||
* 统计日当天委外<strong>发出 + 接收</strong>之和(便于前端单列展示;等于 subcontractLeaveQtyOnReportDay + subcontractEnterQtyOnReportDay,无当日数据时为 null)。
|
||||
*/
|
||||
private Double subcontractLeaveEnterSumOnReportDay;
|
||||
|
||||
/**
|
||||
* 加工车间
|
||||
*/
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package com.ruoyi.system.domain.dto.buildcapacity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BottleneckMaterial {
|
||||
|
||||
private String materialCode;
|
||||
|
||||
private String materialName;
|
||||
|
||||
private Integer canSupportQty;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.ruoyi.system.domain.dto.buildcapacity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BuildCapacityDTO {
|
||||
|
||||
/**
|
||||
* 成品编码(前端选中的成品/产品编码)
|
||||
*/
|
||||
private String productCode;
|
||||
|
||||
/**
|
||||
* 成品名称(可选,仅用于展示)
|
||||
*/
|
||||
private String productName;
|
||||
|
||||
/**
|
||||
* 最大可生产套数(取所有物料行 calc.canSupportQty 的最小值)
|
||||
*/
|
||||
private Integer maxBuildQty;
|
||||
|
||||
/**
|
||||
* 自制物料明细(物料编码非 009 开头)
|
||||
*/
|
||||
private List<MaterialItem> selfMadeMaterials;
|
||||
|
||||
/**
|
||||
* 采购物料明细(物料编码以 009 开头)
|
||||
*/
|
||||
private List<MaterialItem> purchaseMaterials;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.ruoyi.system.domain.dto.buildcapacity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CalcInfo {
|
||||
|
||||
private Integer availableQty;
|
||||
|
||||
private Integer finalAvailableQty;
|
||||
|
||||
private Integer canSupportQty;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.ruoyi.system.domain.dto.buildcapacity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MaterialItem {
|
||||
|
||||
private String materialCode;
|
||||
|
||||
private String materialName;
|
||||
|
||||
private Integer requiredQty;
|
||||
|
||||
private String material;
|
||||
|
||||
private Double singleWeight;
|
||||
|
||||
private Double totalWeight;
|
||||
|
||||
private String remark;
|
||||
|
||||
private String batchQuantity;
|
||||
|
||||
private StockInfo stock;
|
||||
|
||||
private CalcInfo calc;
|
||||
|
||||
private StockStatus status;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.ruoyi.system.domain.dto.buildcapacity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StockInfo {
|
||||
|
||||
/**
|
||||
* 即时库存(当前可用库存台账数量)
|
||||
*/
|
||||
private Integer onHandQty;
|
||||
|
||||
/**
|
||||
* 采购未入库数量
|
||||
*/
|
||||
private Integer poNotInQty;
|
||||
|
||||
/**
|
||||
* 生产订单未入库数量
|
||||
*/
|
||||
private Integer moNotInQty;
|
||||
|
||||
/**
|
||||
* 生产订单未领料数量
|
||||
*/
|
||||
private Integer moNotPickQty;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.ruoyi.system.domain.dto.buildcapacity;
|
||||
|
||||
public enum StockStatus {
|
||||
OK,
|
||||
SHORTAGE
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
package com.ruoyi.system.domain.dto.excuteDrawing;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ProductInfo {
|
||||
//型号名称
|
||||
@ -11,4 +13,8 @@ public class ProductInfo {
|
||||
private String number;
|
||||
//变量信息
|
||||
private DataInfo vars;
|
||||
//是否保留零件图
|
||||
private boolean preservepartdrawing;
|
||||
//企标图子件信息
|
||||
private List<Subparts> subparts;
|
||||
}
|
||||
|
||||
@ -108,4 +108,16 @@ public class FigureSaveVo {
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date uploadTime;
|
||||
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createBy;
|
||||
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updateBy;
|
||||
}
|
||||
|
||||
@ -32,6 +32,12 @@ public class PlanOrderVo {
|
||||
@ExcelProperty(value = "生产令号")
|
||||
@JsonProperty("F_HBYT_SCLH")
|
||||
private String fHbytSclh;
|
||||
/**
|
||||
* 生产令号
|
||||
*/
|
||||
@ExcelProperty(value = "生产令号")
|
||||
@JsonProperty("F_HBYT_XMH")
|
||||
private String fHbytXmh;
|
||||
|
||||
/**
|
||||
* 生产订单编号
|
||||
|
||||
@ -44,7 +44,11 @@ public class ProcessOrderProVo {
|
||||
@ExcelProperty(value = "项目名称")
|
||||
private String productionName;
|
||||
|
||||
|
||||
/**
|
||||
* 是否可以下载
|
||||
*/
|
||||
@ExcelProperty(value = "是否可以下载")
|
||||
private Integer isDownload;
|
||||
/**
|
||||
* 计划结束时间
|
||||
*/
|
||||
@ -185,4 +189,10 @@ public class ProcessOrderProVo {
|
||||
*/
|
||||
private Long delayDay;
|
||||
|
||||
/**
|
||||
* 工艺计划表(钉钉)发送次数
|
||||
*/
|
||||
@ExcelProperty(value = "工艺计划表发送次数")
|
||||
private Integer routePlanSendCount;
|
||||
|
||||
}
|
||||
|
||||
@ -86,16 +86,16 @@ public class ProductionOrderVo {
|
||||
@ExcelProperty(value = "上级部件图号")
|
||||
private String parentDrawingNo;
|
||||
|
||||
/**
|
||||
* 主产品图号
|
||||
*/
|
||||
@ExcelProperty(value = "主产品图号")
|
||||
private String mainProducts;
|
||||
|
||||
/**
|
||||
* 主产品名称
|
||||
*/
|
||||
@ExcelProperty(value = "主产品名称")
|
||||
private String mainProductsName;
|
||||
private String mainName;
|
||||
|
||||
/**
|
||||
* 主产品图号
|
||||
*/
|
||||
@ExcelProperty(value = "主产品图号")
|
||||
private String mainPart;
|
||||
|
||||
}
|
||||
|
||||
@ -50,6 +50,11 @@ public class SfcOperationPlanningMainVo {
|
||||
*/
|
||||
@ExcelProperty(value = "生产令号")
|
||||
private String hbytSclh;
|
||||
/**
|
||||
* 项目号
|
||||
*/
|
||||
@ExcelProperty(value = "项目号")
|
||||
private String fHbytXmh;;
|
||||
|
||||
/**
|
||||
* 产品编码
|
||||
|
||||
@ -19,6 +19,13 @@ public class FSubEntity {
|
||||
@JsonProperty("FOperNumber")
|
||||
private Long FOperNumber;
|
||||
|
||||
/**
|
||||
* 加工车间(部门),与 {@link F_HBYT_RKCK} 相同写法:JSON 为
|
||||
* {@code "FDepartmentId": { "FNumber": "018" }},对应金蝶字段路径 {@code FDepartmentId.FNumber}。
|
||||
*/
|
||||
@JsonProperty("FWorkShopID")
|
||||
private FWorkShopID FWorkShopID;
|
||||
|
||||
@JsonProperty("F_HBYT_ZYRY")
|
||||
private String F_HBYT_ZYRY;
|
||||
|
||||
@ -31,6 +38,12 @@ public class FSubEntity {
|
||||
@JsonProperty("F_HBYT_RKCK")
|
||||
private F_HBYT_RKCK F_HBYT_RKCK;
|
||||
|
||||
@Data
|
||||
public static class FWorkShopID {
|
||||
@JsonProperty("FNUMBER")
|
||||
private String FNUMBER;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class F_HBYT_RKCK {
|
||||
@JsonProperty("FNUMBER")
|
||||
|
||||
@ -20,9 +20,10 @@ public class Model {
|
||||
@JsonProperty("FPlanFinishTime")
|
||||
private Date FPlanFinishTime;
|
||||
|
||||
|
||||
@JsonProperty("FEntity")
|
||||
private List<FEntity> FEntity = new ArrayList<>();
|
||||
|
||||
// getters and setters
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,10 @@ import com.ruoyi.system.domain.ProductionOrder;
|
||||
import com.ruoyi.system.domain.vo.ProductionOrderVo;
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 生产订单Mapper接口
|
||||
@ -14,4 +18,23 @@ import org.apache.ibatis.annotations.Delete;
|
||||
public interface ProductionOrderMapper extends BaseMapperPlus<ProductionOrderMapper, ProductionOrder, ProductionOrderVo> {
|
||||
@Delete("DELETE FROM production_order where production_order_no = #{productionOrderNo}")
|
||||
void deleteWithProCode(String productionOrderNo);
|
||||
|
||||
@Select("SELECT DISTINCT production_order_no " +
|
||||
"FROM production_order " +
|
||||
"WHERE production_order_no IS NOT NULL " +
|
||||
" AND production_order_no <> '' " +
|
||||
" AND (#{keyword} IS NULL OR #{keyword} = '' OR production_order_no LIKE CONCAT('%', #{keyword}, '%')) " +
|
||||
"ORDER BY production_order_no DESC " +
|
||||
"LIMIT #{limit}")
|
||||
List<String> selectDistinctProductionOrderNos(@Param("keyword") String keyword, @Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 按图号批量查询单重
|
||||
*/
|
||||
List<ProductionOrder> selectWeightByDrawingNos(@Param("drawingNos") List<String> drawingNos);
|
||||
|
||||
/**
|
||||
* 按物料编码批量查询工艺路线单重
|
||||
*/
|
||||
List<ProductionOrder> selectDiscWeightByMaterialCodes(@Param("materialCodes") List<String> materialCodes);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.ruoyi.system.runner;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@ -541,6 +542,298 @@ public class JdUtil {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶报表部分数值列以字符串返回,且可能含千分位逗号(如 1,176),需规范化后再转 BigDecimal。
|
||||
*/
|
||||
private static String normalizeKingdeeNumericString(String raw) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
return raw.trim()
|
||||
.replace(",", "")
|
||||
.replace(",", "")
|
||||
.replace("\u00A0", "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
private static BigDecimal getBigDecimalFromJsonArray(JsonArray array, int index) {
|
||||
if (array.size() <= index || array.get(index).isJsonNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonElement el = array.get(index);
|
||||
String raw;
|
||||
if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isNumber()) {
|
||||
raw = el.getAsJsonPrimitive().getAsString();
|
||||
} else {
|
||||
raw = el.getAsString();
|
||||
}
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String value = normalizeKingdeeNumericString(raw);
|
||||
if (value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("报表数量解析失败(规范化后仍非法): raw={} normalized={}", raw, value);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工序汇报汇总报表(系统报表 GetSysReportData)。
|
||||
* FieldKeys 顺序需与 {@link KingdeeOperationReportSumRowDTO} 字段一致(默认 11 列)。
|
||||
*
|
||||
* @param formId 报表 FormId(金蝶报表属性中查看)
|
||||
* @param fieldKeys 输出列,逗号分隔
|
||||
* @param beginDateTime FBeginDate,如 2026-04-10 00:00:00
|
||||
* @param endDateTime FEndDate,如 2026-05-10 23:59:59
|
||||
* @param processOrgNumber 加工组织 FNumber,如 100
|
||||
* @param timeUnit Model.FTimeUnit,如 B 按日
|
||||
* @param moBillNo 可选,单个生产订单号过滤 FMOBillNo
|
||||
* @param limitPerPage 每页 Limit,内部会分页累加
|
||||
*/
|
||||
public static List<KingdeeOperationReportSumRowDTO> getOperationReportSumRpt(
|
||||
String formId,
|
||||
String fieldKeys,
|
||||
String beginDateTime,
|
||||
String endDateTime,
|
||||
String processOrgNumber,
|
||||
String timeUnit,
|
||||
String moBillNo,
|
||||
int limitPerPage) {
|
||||
List<KingdeeOperationReportSumRowDTO> all = new ArrayList<>();
|
||||
if (formId == null || formId.isEmpty()) {
|
||||
log.warn("工序汇报汇总报表 FormId 为空");
|
||||
return all;
|
||||
}
|
||||
int limit = Math.min(Math.max(limitPerPage, 1), 10000);
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
Gson gson = new Gson();
|
||||
|
||||
JsonObject template = new JsonObject();
|
||||
template.addProperty("FieldKeys", fieldKeys != null && !fieldKeys.isEmpty() ? fieldKeys
|
||||
: "FDEPARTMENTID,FDATE,FMOBillNo,FOpOper,FWORKCENTERID,FPROCESSID,FPRODUCTNUMBER,FMATERIALNAME,FPRDFINISHQTY,FPRDQUAQTY,FPRDFAILQTY");
|
||||
template.addProperty("SchemeId", "");
|
||||
template.addProperty("IsVerifyBaseDataField", "true");
|
||||
|
||||
JsonObject model = new JsonObject();
|
||||
JsonObject fProcessOrgId = new JsonObject();
|
||||
fProcessOrgId.addProperty("FNumber", processOrgNumber != null && !processOrgNumber.isEmpty() ? processOrgNumber : "100");
|
||||
model.add("FProcessOrgId", fProcessOrgId);
|
||||
model.addProperty("FBeginDate", beginDateTime);
|
||||
model.addProperty("FEndDate", endDateTime);
|
||||
model.addProperty("FTimeUnit", timeUnit != null && !timeUnit.isEmpty() ? timeUnit : "B");
|
||||
template.add("Model", model);
|
||||
|
||||
JsonArray filterString = new JsonArray();
|
||||
if (moBillNo != null && !moBillNo.trim().isEmpty()) {
|
||||
JsonObject f = new JsonObject();
|
||||
f.addProperty("Left", "");
|
||||
f.addProperty("FieldName", "FMOBillNo");
|
||||
f.addProperty("Compare", "67");
|
||||
f.addProperty("Value", moBillNo.trim());
|
||||
f.addProperty("Right", "");
|
||||
f.addProperty("Logic", 0);
|
||||
filterString.add(f);
|
||||
}
|
||||
template.add("FilterString", filterString);
|
||||
|
||||
int startRow = 0;
|
||||
try {
|
||||
while (true) {
|
||||
JsonObject page = gson.fromJson(template.toString(), JsonObject.class);
|
||||
page.addProperty("StartRow", startRow);
|
||||
page.addProperty("Limit", limit);
|
||||
|
||||
String resultJson = client.getSysReportData(formId, page.toString());
|
||||
if (resultJson == null || resultJson.isEmpty()) {
|
||||
log.warn("工序汇报汇总报表返回空 formId={} startRow={}", formId, startRow);
|
||||
break;
|
||||
}
|
||||
JsonObject root = new JsonParser().parse(resultJson).getAsJsonObject();
|
||||
if (!root.has("Result")) {
|
||||
log.warn("工序汇报汇总报表无 Result 节点: {}", trunc(resultJson));
|
||||
break;
|
||||
}
|
||||
JsonObject result = root.getAsJsonObject("Result");
|
||||
if (result.has("ResponseStatus")) {
|
||||
JsonObject rs = result.getAsJsonObject("ResponseStatus");
|
||||
if (rs.has("IsSuccess") && !rs.get("IsSuccess").getAsBoolean()) {
|
||||
log.error("工序汇报汇总报表业务失败: {}", trunc(resultJson));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!result.has("Rows")) {
|
||||
break;
|
||||
}
|
||||
JsonArray rows = result.getAsJsonArray("Rows");
|
||||
if (rows == null || rows.size() == 0) {
|
||||
break;
|
||||
}
|
||||
for (JsonElement rowElement : rows) {
|
||||
if (rowElement == null || rowElement.isJsonNull() || !rowElement.isJsonArray()) {
|
||||
continue;
|
||||
}
|
||||
JsonArray row = rowElement.getAsJsonArray();
|
||||
KingdeeOperationReportSumRowDTO dto = mapOperationReportSumRow(row);
|
||||
all.add(dto);
|
||||
}
|
||||
if (rows.size() < limit) {
|
||||
break;
|
||||
}
|
||||
startRow += limit;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("工序汇报汇总报表调用异常 formId={}", formId, e);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
private static KingdeeOperationReportSumRowDTO mapOperationReportSumRow(JsonArray row) {
|
||||
KingdeeOperationReportSumRowDTO dto = new KingdeeOperationReportSumRowDTO();
|
||||
dto.setDepartmentId(getStringFromJsonArray(row, 0));
|
||||
dto.setReportDate(getStringFromJsonArray(row, 1));
|
||||
dto.setMoBillNo(getStringFromJsonArray(row, 2));
|
||||
dto.setOpOper(getStringFromJsonArray(row, 3));
|
||||
dto.setWorkCenterId(getStringFromJsonArray(row, 4));
|
||||
dto.setProcessId(getStringFromJsonArray(row, 5));
|
||||
dto.setProductNumber(getStringFromJsonArray(row, 6));
|
||||
dto.setMaterialName(getStringFromJsonArray(row, 7));
|
||||
dto.setFinishQty(getBigDecimalFromJsonArray(row, 8));
|
||||
dto.setQuaQty(getBigDecimalFromJsonArray(row, 9));
|
||||
dto.setFailQty(getBigDecimalFromJsonArray(row, 10));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static String trunc(String s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
return s.length() > 500 ? s.substring(0, 500) + "..." : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工序委外转移报表(系统报表 GetSysReportData)。
|
||||
* Model:FProOrgId、FStartDate、FFinishDate、FDocumentStatus。
|
||||
* Result 可能含顶层 {@code IsSuccess}(与 ResponseStatus 并存)。
|
||||
*/
|
||||
public static List<KingdeeSubcontractTransferRowDTO> getSubcontractTransferRpt(
|
||||
String formId,
|
||||
String fieldKeys,
|
||||
String startDateTime,
|
||||
String finishDateTime,
|
||||
String proOrgNumber,
|
||||
String documentStatus,
|
||||
String productOrderCode,
|
||||
int limitPerPage) {
|
||||
List<KingdeeSubcontractTransferRowDTO> all = new ArrayList<>();
|
||||
if (formId == null || formId.isEmpty()) {
|
||||
log.warn("工序委外转移报表 FormId 为空");
|
||||
return all;
|
||||
}
|
||||
int limit = Math.min(Math.max(limitPerPage, 1), 10000);
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
Gson gson = new Gson();
|
||||
|
||||
JsonObject template = new JsonObject();
|
||||
template.addProperty("FieldKeys", fieldKeys != null && !fieldKeys.isEmpty() ? fieldKeys
|
||||
: "FProductNumber,FProductName,FProviderName,FProductOrderCodes,FDates,FShiftTaskName,FLeaveQty,FEnterQty,FQualifiedQty");
|
||||
template.addProperty("SchemeId", "");
|
||||
template.addProperty("IsVerifyBaseDataField", "true");
|
||||
|
||||
JsonObject model = new JsonObject();
|
||||
JsonObject fProOrgId = new JsonObject();
|
||||
fProOrgId.addProperty("FNumber", proOrgNumber != null && !proOrgNumber.isEmpty() ? proOrgNumber : "100");
|
||||
model.add("FProOrgId", fProOrgId);
|
||||
model.addProperty("FStartDate", startDateTime);
|
||||
model.addProperty("FFinishDate", finishDateTime);
|
||||
model.addProperty("FDocumentStatus", documentStatus != null && !documentStatus.isEmpty() ? documentStatus : "C");
|
||||
template.add("Model", model);
|
||||
|
||||
JsonArray filterString = new JsonArray();
|
||||
if (productOrderCode != null && !productOrderCode.trim().isEmpty()) {
|
||||
JsonObject f = new JsonObject();
|
||||
f.addProperty("Left", "");
|
||||
f.addProperty("FieldName", "FProductOrderCodes");
|
||||
f.addProperty("Compare", "67");
|
||||
f.addProperty("Value", productOrderCode.trim());
|
||||
f.addProperty("Right", "");
|
||||
f.addProperty("Logic", 0);
|
||||
filterString.add(f);
|
||||
}
|
||||
template.add("FilterString", filterString);
|
||||
|
||||
int startRow = 0;
|
||||
try {
|
||||
while (true) {
|
||||
JsonObject page = gson.fromJson(template.toString(), JsonObject.class);
|
||||
page.addProperty("StartRow", startRow);
|
||||
page.addProperty("Limit", limit);
|
||||
|
||||
String resultJson = client.getSysReportData(formId, page.toString());
|
||||
if (resultJson == null || resultJson.isEmpty()) {
|
||||
log.warn("工序委外转移报表返回空 formId={} startRow={}", formId, startRow);
|
||||
break;
|
||||
}
|
||||
JsonObject root = new JsonParser().parse(resultJson).getAsJsonObject();
|
||||
if (!root.has("Result")) {
|
||||
log.warn("工序委外转移报表无 Result: {}", trunc(resultJson));
|
||||
break;
|
||||
}
|
||||
JsonObject result = root.getAsJsonObject("Result");
|
||||
if (result.has("IsSuccess") && !result.get("IsSuccess").getAsBoolean()) {
|
||||
log.error("工序委外转移报表 IsSuccess=false: {}", trunc(resultJson));
|
||||
break;
|
||||
}
|
||||
if (result.has("ResponseStatus")) {
|
||||
JsonObject rs = result.getAsJsonObject("ResponseStatus");
|
||||
if (rs.has("IsSuccess") && !rs.get("IsSuccess").getAsBoolean()) {
|
||||
log.error("工序委外转移报表 ResponseStatus 失败: {}", trunc(resultJson));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!result.has("Rows")) {
|
||||
break;
|
||||
}
|
||||
JsonArray rows = result.getAsJsonArray("Rows");
|
||||
if (rows == null || rows.size() == 0) {
|
||||
break;
|
||||
}
|
||||
for (JsonElement rowElement : rows) {
|
||||
if (rowElement == null || rowElement.isJsonNull() || !rowElement.isJsonArray()) {
|
||||
continue;
|
||||
}
|
||||
JsonArray row = rowElement.getAsJsonArray();
|
||||
all.add(mapSubcontractTransferRow(row));
|
||||
}
|
||||
if (rows.size() < limit) {
|
||||
break;
|
||||
}
|
||||
startRow += limit;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("工序委外转移报表调用异常 formId={}", formId, e);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
private static KingdeeSubcontractTransferRowDTO mapSubcontractTransferRow(JsonArray row) {
|
||||
KingdeeSubcontractTransferRowDTO dto = new KingdeeSubcontractTransferRowDTO();
|
||||
dto.setProductNumber(getStringFromJsonArray(row, 0));
|
||||
dto.setProductName(getStringFromJsonArray(row, 1));
|
||||
dto.setProviderName(getStringFromJsonArray(row, 2));
|
||||
dto.setProductOrderCodes(getStringFromJsonArray(row, 3));
|
||||
dto.setDocumentDate(getStringFromJsonArray(row, 4));
|
||||
dto.setShiftTaskName(getStringFromJsonArray(row, 5));
|
||||
dto.setLeaveQty(getBigDecimalFromJsonArray(row, 6));
|
||||
dto.setEnterQty(getBigDecimalFromJsonArray(row, 7));
|
||||
dto.setQualifiedQty(getBigDecimalFromJsonArray(row, 8));
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询生产订单单据体
|
||||
* 查询
|
||||
@ -1597,167 +1890,6 @@ public class JdUtil {
|
||||
}
|
||||
}
|
||||
|
||||
//新增父级物料
|
||||
public static int loadChengPinMaterialPreservation(BomDetails bomDetail, Double fbWorkTime) {
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
// 创建一个空的JsonObject
|
||||
JsonObject json = new JsonObject();
|
||||
|
||||
// 添加IsAutoSubmitAndAudit字段
|
||||
json.addProperty("IsAutoSubmitAndAudit", "true");
|
||||
|
||||
// 创建Model对象,并加入JsonObject
|
||||
JsonObject model = new JsonObject();
|
||||
json.add("Model", model);
|
||||
|
||||
// 添加Model字段
|
||||
model.addProperty("FMATERIALID", 0);
|
||||
if (!(bomDetail.getDanZhong() == null)) {
|
||||
model.addProperty("F_HBYT_DZ", bomDetail.getDanZhong());
|
||||
}
|
||||
model.addProperty("FNumber", bomDetail.getFNumber());
|
||||
model.addProperty("FName", bomDetail.getFName());
|
||||
|
||||
// 创建FMaterialGroup对象,并加入Model
|
||||
JsonObject fMaterialGroup = new JsonObject();
|
||||
fMaterialGroup.addProperty("FNumber", "A000106");
|
||||
model.add("FMaterialGroup", fMaterialGroup);
|
||||
|
||||
model.addProperty("FIsHandleReserve", true);
|
||||
|
||||
// 创建SubHeadEntity对象,并加入Model
|
||||
JsonObject subHeadEntity = new JsonObject();
|
||||
model.add("SubHeadEntity", subHeadEntity);
|
||||
|
||||
subHeadEntity.addProperty("FErpClsID", "2");
|
||||
subHeadEntity.addProperty("FFeatureItem", "1");
|
||||
|
||||
// 创建FCategoryID对象,并加入SubHeadEntity
|
||||
JsonObject fCategoryID = new JsonObject();
|
||||
fCategoryID.addProperty("FNumber", "CHLB05_SYS");// 产成品
|
||||
subHeadEntity.add("FCategoryID", fCategoryID);
|
||||
|
||||
// 创建FTaxRateId对象,并加入SubHeadEntity
|
||||
JsonObject fTaxRateId = new JsonObject();
|
||||
fTaxRateId.addProperty("FNUMBER", "SL02_SYS");
|
||||
subHeadEntity.add("FTaxRateId", fTaxRateId);
|
||||
|
||||
// 创建FBaseUnitId对象,并加入SubHeadEntity
|
||||
JsonObject fBaseUnitId = new JsonObject();
|
||||
fBaseUnitId.addProperty("FNumber", "008");
|
||||
subHeadEntity.add("FBaseUnitId", fBaseUnitId);
|
||||
|
||||
subHeadEntity.addProperty("FIsPurchase", true);
|
||||
subHeadEntity.addProperty("FIsInventory", true);
|
||||
// 成品不允许委外
|
||||
// subHeadEntity.addProperty("FIsSubContract", true);
|
||||
subHeadEntity.addProperty("FIsSale", true);
|
||||
subHeadEntity.addProperty("FIsProduce", true);
|
||||
|
||||
// 创建SubHeadEntity1对象,并加入Model
|
||||
JsonObject subHeadEntity1 = new JsonObject();
|
||||
model.add("SubHeadEntity1", subHeadEntity1);
|
||||
JsonObject fStoreUnitId = new JsonObject();
|
||||
fStoreUnitId.addProperty("FNumber", "008");
|
||||
subHeadEntity1.add("FStoreUnitID", fStoreUnitId);
|
||||
subHeadEntity1.addProperty("FUnitConvertDir", "1");
|
||||
|
||||
// 创建FStockId对象,并加入SubHeadEntity1
|
||||
JsonObject fStockId = new JsonObject();
|
||||
// 判断是产成品还是企标
|
||||
String cangKu = "CK011";
|
||||
fStockId.addProperty("FNumber", cangKu);
|
||||
subHeadEntity1.add("FStockId", fStockId);
|
||||
|
||||
// 创建FCurrencyId对象,并加入SubHeadEntity1
|
||||
JsonObject fCurrencyId = new JsonObject();
|
||||
fCurrencyId.addProperty("FNumber", "PRE001");
|
||||
subHeadEntity1.add("FCurrencyId", fCurrencyId);
|
||||
|
||||
subHeadEntity1.addProperty("FIsSNPRDTracy", false);
|
||||
subHeadEntity1.addProperty("FSNManageType", "1");
|
||||
subHeadEntity1.addProperty("FSNGenerateTime", "1");
|
||||
subHeadEntity1.addProperty("FBoxStandardQty", 0.0);
|
||||
// 创建FPurchaseUnitId对象,并加入SubHeadEntity1
|
||||
JsonObject fPurchaseUnitId = new JsonObject();
|
||||
fPurchaseUnitId.addProperty("FNumber", "008");
|
||||
subHeadEntity1.add("FPurchaseUnitId", fPurchaseUnitId);
|
||||
|
||||
// 创建SubHeadEntity3对象,并加入Model
|
||||
JsonObject subHeadEntity3 = new JsonObject();
|
||||
model.add("SubHeadEntity3", subHeadEntity3);
|
||||
|
||||
|
||||
// 创建FPurchasePriceUnitId对象,并加入SubHeadEntity3
|
||||
JsonObject fPurchasePriceUnitId = new JsonObject();
|
||||
fPurchasePriceUnitId.addProperty("FNumber", "008");
|
||||
subHeadEntity3.add("FPurchasePriceUnitId", fPurchasePriceUnitId);
|
||||
|
||||
subHeadEntity3.addProperty("FIsQuota", false);
|
||||
subHeadEntity3.addProperty("FQuotaType", "1");
|
||||
|
||||
// 创建SubHeadEntity6对象,并加入Model 检验项
|
||||
JsonObject subHeadEntity6 = new JsonObject();
|
||||
model.add("SubHeadEntity6", subHeadEntity6);
|
||||
subHeadEntity6.addProperty("FCheckProduct", true);
|
||||
subHeadEntity6.addProperty("FCheckReturn", true);
|
||||
|
||||
// 创建SubHeadEntity5对象,并加入Model
|
||||
JsonObject subHeadEntity5 = new JsonObject();
|
||||
model.add("SubHeadEntity5", subHeadEntity5);
|
||||
|
||||
// 创建FProduceUnitId对象,并加入SubHeadEntity5
|
||||
JsonObject fProduceUnitId = new JsonObject();
|
||||
fProduceUnitId.addProperty("FNumber", "008");
|
||||
subHeadEntity5.add("FProduceUnitId", fProduceUnitId);
|
||||
|
||||
// 创建FProduceBillType对象,并加入SubHeadEntity5
|
||||
JsonObject fProduceBillType = new JsonObject();
|
||||
fProduceBillType.addProperty("FNUMBER", "SCDD05_SYS");
|
||||
subHeadEntity5.add("FProduceBillType", fProduceBillType);
|
||||
|
||||
// 创建FBOMUnitId对象,并加入SubHeadEntity5
|
||||
JsonObject fBOMUnitId = new JsonObject();
|
||||
fBOMUnitId.addProperty("FNumber", "008");
|
||||
subHeadEntity5.add("FBOMUnitId", fBOMUnitId);
|
||||
|
||||
subHeadEntity5.addProperty("FIsMainPrd", true);
|
||||
subHeadEntity5.addProperty("FIssueType", "1");
|
||||
|
||||
// 创建FPickStockId对象,并加入SubHeadEntity5
|
||||
JsonObject fPickStockId = new JsonObject();
|
||||
fPickStockId.addProperty("FNumber", "CK012");
|
||||
subHeadEntity1.add("FPickStockId", fPickStockId);
|
||||
|
||||
subHeadEntity5.addProperty("FOverControlMode", "1");
|
||||
subHeadEntity5.addProperty("FStdLaborProcessTime", fbWorkTime);
|
||||
subHeadEntity5.addProperty("FStandHourUnitId", "60");
|
||||
subHeadEntity5.addProperty("FBackFlushType", "1");
|
||||
String jsonData = json.toString();
|
||||
System.out.println(jsonData);
|
||||
try {
|
||||
// 业务对象标识
|
||||
String formId = "BD_MATERIAL";
|
||||
// 调用接口
|
||||
String resultJson = client.save(formId, jsonData);
|
||||
// 用于记录结果
|
||||
Gson gson = new Gson();
|
||||
// 对返回结果进行解析和校验
|
||||
RepoRet repoRet = gson.fromJson(resultJson, RepoRet.class);
|
||||
if (repoRet.getResult().getResponseStatus().isIsSuccess()) {
|
||||
log.debug("接口返回结果: %s%n" + gson.toJson(repoRet.getResult()));
|
||||
return 1;
|
||||
} else {
|
||||
MessageUtil.fail("接口返回结果: " + gson.toJson(repoRet.getResult().getResponseStatus()));
|
||||
log.error("接口返回结果: " + gson.toJson(repoRet.getResult().getResponseStatus()));
|
||||
return 0;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
MessageUtil.fail(e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//查询物料 作用于更新到物料的 仓位 是否启用VMI
|
||||
public static List<JdEntryVmi> getEntryID2(String materialCode) {
|
||||
@ -3671,7 +3803,7 @@ public class JdUtil {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("FormId", "SFC_OperationPlanning");
|
||||
json.addProperty("FieldKeys",
|
||||
"FID,FENTITY_FEntryID,FSubEntity_FDetailID,F_HBYT_SCLH,FMONumber,FProductId.FNumber,FProductName,FProSpecification,FBillNo,FOperNumber," +
|
||||
"FID,FENTITY_FEntryID,FSubEntity_FDetailID,F_HBYT_SCLH,F_HBYT_XMH,FMONumber,FProductId.FNumber,FProductName,FProSpecification,FBillNo,FOperNumber," +
|
||||
"FOperQty,FQualifiedQty,FTransOutQty,FUnqualifiedQty,FWastageQty,FTransInQty,FDepartmentId.FName,FWorkCenterId.FName," +
|
||||
"FProcessId.FName,F_HBYT_ZYRY,FOperStatus,FSeqPlanStartTime,FSeqPlanFinishTime,FOperPlanStartTime,FOperPlanFinishTime," +
|
||||
"FOptCtrlCodeId.FName,FKeyOper,FActivity1Name,FOperDescription,FPlanStartTime,FPlanFinishTime,F_HBYT_SB");
|
||||
@ -3688,7 +3820,7 @@ public class JdUtil {
|
||||
JsonObject filterObject1 = new JsonObject();
|
||||
filterObject1.addProperty("FieldName", "FPlanFinishTime");
|
||||
filterObject1.addProperty("Compare", "39");
|
||||
filterObject1.addProperty("Value", "2026-04-15 00:00:00");
|
||||
filterObject1.addProperty("Value", "2026-04-16 00:00:00");
|
||||
filterObject1.addProperty("Left", "");
|
||||
filterObject1.addProperty("Right", "");
|
||||
filterObject1.addProperty("Logic", 0);
|
||||
@ -3810,6 +3942,7 @@ public class JdUtil {
|
||||
OperationPlanResultDTO resultDTO = new OperationPlanResultDTO();
|
||||
resultDTO.setMoNumber(moNumber);
|
||||
resultDTO.setBatchNo(firstItem.getFHbytSclh()); // 生产令号
|
||||
resultDTO.setProjectNo(firstItem.getFHbytXmh());
|
||||
resultDTO.setProductCode(firstItem.getFProductIdFNumber());
|
||||
resultDTO.setProductName(firstItem.getFProductName());
|
||||
resultDTO.setBillNo(firstItem.getFBillNo());
|
||||
|
||||
@ -85,6 +85,9 @@ public class JsonConverter {
|
||||
processRouteDTO.getWorkCenter().equals("铆焊工段") ||
|
||||
processRouteDTO.getWorkCenter().equals("委外中心")) {
|
||||
department.setFNUMBER("006");
|
||||
} else if (processRouteDTO.getWorkCenter().equals("下料工段")||
|
||||
processRouteDTO.getWorkCenter().equals("焊接工段")){
|
||||
department.setFNUMBER("018");
|
||||
}else{
|
||||
department.setFNUMBER("007");
|
||||
}
|
||||
|
||||
@ -19,10 +19,10 @@ import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
@ -35,10 +35,25 @@ public class PDFGenerator {
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
||||
static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* PDFBox 分页时在方法内 {@code close()} 旧流并新建流后,必须把新流回传给调用方;
|
||||
* 否则调用方仍握着已关闭的流,而新流永不关闭,save 时会报 Cannot read while there is an open stream writer。
|
||||
*/
|
||||
private static final class PdfDrawContext {
|
||||
PDPageContentStream stream;
|
||||
PDPage page;
|
||||
|
||||
PdfDrawContext(PDPageContentStream stream, PDPage page) {
|
||||
this.stream = stream;
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制表格行的方法
|
||||
private static PDPageContentStream drawTableRow(PDPageContentStream contentStream, PDType0Font font, float margin, float yStart,
|
||||
private static void drawTableRow(PdfDrawContext ctx, PDType0Font font, float margin, float yStart,
|
||||
float tableWidth, float[] colWidths, String[] cells, PDDocument document,
|
||||
PDPage page, float rowHeight, float pageHeight) throws IOException {
|
||||
float rowHeight, float pageHeight) throws IOException {
|
||||
PDPageContentStream contentStream = ctx.stream;
|
||||
float nextX = margin;
|
||||
float nextY = yStart;
|
||||
|
||||
@ -47,10 +62,11 @@ public class PDFGenerator {
|
||||
// 检查是否需要分页
|
||||
if (nextY - rowHeight < margin) {
|
||||
contentStream.close();
|
||||
// 创建新页面
|
||||
page = new PDPage();
|
||||
document.addPage(page);
|
||||
contentStream = new PDPageContentStream(document, page);
|
||||
PDPage newPage = new PDPage();
|
||||
document.addPage(newPage);
|
||||
contentStream = new PDPageContentStream(document, newPage);
|
||||
ctx.stream = contentStream;
|
||||
ctx.page = newPage;
|
||||
// 重置Y坐标为新页面顶部
|
||||
nextY = pageHeight - margin;
|
||||
}
|
||||
@ -73,8 +89,6 @@ public class PDFGenerator {
|
||||
|
||||
nextX += colWidths[i];
|
||||
}
|
||||
|
||||
return contentStream; // 返回可能更新后的contentStream
|
||||
}
|
||||
|
||||
// 生成二维码
|
||||
@ -94,8 +108,19 @@ public class PDFGenerator {
|
||||
return LosslessFactory.createFromImage(document, bufferedImage);
|
||||
}
|
||||
|
||||
public static String writeToPdf(List<CombinedDTO> combinedVoList, String rooteProdet) throws IOException {
|
||||
String fontPath = "C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf";
|
||||
public static String writeToPdf(List<CombinedDTO> combinedVoList, String rooteProdet) {
|
||||
return writeToPdf(combinedVoList, rooteProdet, Arrays.asList(
|
||||
"C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf"
|
||||
));
|
||||
}
|
||||
|
||||
public static String writeToPdf(List<CombinedDTO> combinedVoList, String rooteProdet, List<String> fontPaths) {
|
||||
String[] fontCandidates = (fontPaths == null || fontPaths.isEmpty())
|
||||
? new String[] {
|
||||
"C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf"
|
||||
}
|
||||
: fontPaths.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()).toArray(String[]::new);
|
||||
List<String> pdfPaths = new ArrayList<>();
|
||||
String directoryPath = "D:\\上传BOM\\" + rooteProdet;
|
||||
// 检查目录是否存在,如果不存在则创建
|
||||
File directory = new File(directoryPath);
|
||||
@ -125,399 +150,36 @@ public class PDFGenerator {
|
||||
throw new RuntimeException("清理目录失败: " + directoryPath + ", " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
Map<String, List<CombinedDTO>> groupMap = new LinkedHashMap<>();
|
||||
if (combinedVoList != null) {
|
||||
for (CombinedDTO dto : combinedVoList) {
|
||||
if (dto == null) {
|
||||
continue;
|
||||
}
|
||||
String prefix = getMaterialPrefix(dto.getMaterialCode());
|
||||
groupMap.computeIfAbsent(prefix, k -> new ArrayList<>()).add(dto);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<CombinedDTO>> entry : groupMap.entrySet()) {
|
||||
List<CombinedDTO> groupList = entry.getValue();
|
||||
if (groupList == null || groupList.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (groupList.size() == 1) {
|
||||
createSinglePdf(groupList.get(0), rooteProdet, directoryPath, fontPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (groupList.size() == 2) {
|
||||
generateTwoInOnePage(groupList.get(0), groupList.get(1), rooteProdet, directoryPath, fontPath, entry.getKey());
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> groupPdfPaths = new ArrayList<>();
|
||||
for (CombinedDTO combinedVo : groupList) {
|
||||
String pdfPath = createSinglePdf(combinedVo, rooteProdet, directoryPath, fontPath);
|
||||
if (pdfPath != null && !pdfPath.trim().isEmpty()) {
|
||||
groupPdfPaths.add(pdfPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupPdfPaths.size() > 1) {
|
||||
String mergedPath = buildMergedPdfPath(groupList.get(0), directoryPath, entry.getKey());
|
||||
Files.move(Paths.get(groupPdfPaths.get(0)), Paths.get(mergedPath), StandardCopyOption.REPLACE_EXISTING);
|
||||
for (int i = 1; i < groupPdfPaths.size(); i++) {
|
||||
String p = groupPdfPaths.get(i);
|
||||
mergePdfToExisting(p, mergedPath);
|
||||
Files.deleteIfExists(Paths.get(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 生成ZIP文件
|
||||
String zipFilePath = directoryPath + "\\" + rooteProdet + "_工序计划单.zip";
|
||||
try {
|
||||
createZipFile(directoryPath, zipFilePath); // 传入整个目录
|
||||
return zipFilePath;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static String getMaterialPrefix(String materialCode) {
|
||||
if (materialCode == null) {
|
||||
return "";
|
||||
}
|
||||
int index = materialCode.indexOf("-");
|
||||
if (index > 0) {
|
||||
return materialCode.substring(0, index);
|
||||
}
|
||||
return materialCode;
|
||||
}
|
||||
|
||||
private static String getDepartmentName(CombinedDTO combinedVo) {
|
||||
List<PlannedProcessVo> processes = combinedVo.getProcesses();
|
||||
if (processes != null && !processes.isEmpty()) {
|
||||
String d = processes.get(0).getFWorkCenterName();
|
||||
return d != null ? d : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String buildMergedPdfPath(CombinedDTO anyVo, String directoryPath, String prefix) {
|
||||
String dept = getDepartmentName(anyVo);
|
||||
String safePrefix = prefix != null ? prefix.replace("/", "_") : "";
|
||||
String fileName = (safePrefix.isEmpty() ? ("UNKNOWN_" + System.currentTimeMillis()) : safePrefix) + ".pdf";
|
||||
|
||||
String folderName = dept;
|
||||
if (folderName == null || folderName.trim().isEmpty()) {
|
||||
String[] parts = fileName.split("_");
|
||||
folderName = parts.length > 0 ? parts[0] : "merge";
|
||||
}
|
||||
|
||||
File departmentFolder = new File(directoryPath, folderName);
|
||||
if (!departmentFolder.exists()) {
|
||||
departmentFolder.mkdirs();
|
||||
}
|
||||
return departmentFolder.getAbsolutePath() + "\\" + fileName;
|
||||
}
|
||||
|
||||
private static String generateTwoInOnePage(CombinedDTO dto1, CombinedDTO dto2, String rooteProdet, String directoryPath, String fontPath, String prefix) {
|
||||
if (dto1 == null || dto2 == null) {
|
||||
return null;
|
||||
}
|
||||
String outputPath = buildMergedPdfPath(dto1, directoryPath, prefix);
|
||||
PDDocument document = null;
|
||||
PDPageContentStream contentStream = null;
|
||||
try {
|
||||
document = new PDDocument();
|
||||
PDPage page = new PDPage();
|
||||
document.addPage(page);
|
||||
|
||||
PDType0Font font = PDType0Font.load(document, new File(fontPath));
|
||||
contentStream = new PDPageContentStream(document, page);
|
||||
|
||||
float topStart = 780f;
|
||||
float bottomStart = 380f;
|
||||
drawPlanIntoArea(dto1, document, page, contentStream, font, rooteProdet, topStart, 400f);
|
||||
drawPlanIntoArea(dto2, document, page, contentStream, font, rooteProdet, bottomStart, 15f);
|
||||
|
||||
contentStream.close();
|
||||
contentStream = null;
|
||||
document.save(outputPath);
|
||||
return outputPath;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
if (contentStream != null) {
|
||||
try {
|
||||
contentStream.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
if (document != null) {
|
||||
try {
|
||||
document.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawPlanIntoArea(CombinedDTO combinedVo, PDDocument document, PDPage page, PDPageContentStream contentStream,
|
||||
PDType0Font font, String rooteProdet, float titleY, float minY) throws IOException, WriterException {
|
||||
float margin = 15f;
|
||||
float rowHeight = 14f;
|
||||
float fontSize = 8f;
|
||||
float pageWidth = page.getMediaBox().getWidth();
|
||||
float qrSize = 90f;
|
||||
float qrGap = 5f;
|
||||
float tableWidth = pageWidth - 2 * margin - (qrSize + qrGap);
|
||||
float qrX = pageWidth - margin - qrSize;
|
||||
|
||||
List<PlannedProcessVo> processes = combinedVo.getProcesses();
|
||||
String productionOrderNumber;
|
||||
Date starttime;
|
||||
Date endtime;
|
||||
Long fmoQty;
|
||||
String fDepartmentName;
|
||||
if (processes != null && !processes.isEmpty()) {
|
||||
productionOrderNumber = String.valueOf(processes.get(0).getFBillNo());
|
||||
starttime = processes.get(0).getFPlanStartTime();
|
||||
endtime = processes.get(0).getFPlanFinishTime();
|
||||
fmoQty = processes.get(0).getFMOQty();
|
||||
fDepartmentName = processes.get(0).getFWorkCenterName();
|
||||
} else {
|
||||
productionOrderNumber = "A";
|
||||
starttime = new Date();
|
||||
endtime = new Date();
|
||||
fmoQty = 0L;
|
||||
fDepartmentName = "";
|
||||
}
|
||||
|
||||
String materialCodeQr;
|
||||
if (combinedVo.getMaterialUsageDTOList() != null && !combinedVo.getMaterialUsageDTOList().isEmpty()) {
|
||||
materialCodeQr = String.valueOf(combinedVo.getMaterialUsageDTOList().get(0).getBillNumber());
|
||||
} else {
|
||||
materialCodeQr = "0";
|
||||
}
|
||||
|
||||
if (titleY - qrSize >= minY) {
|
||||
PDImageXObject qrCode1 = generateQRCode(document, productionOrderNumber, (int) qrSize, (int) qrSize);
|
||||
contentStream.drawImage(qrCode1, qrX, titleY - qrSize + 5);
|
||||
}
|
||||
|
||||
SimpleDateFormat formatte11r = new SimpleDateFormat("yyyy-MM-dd");
|
||||
float[] colWidths = scaleWidths(new float[] { 60, 95, 110, 110, 60, 60, 55 }, tableWidth);
|
||||
float yStart = titleY - 5;
|
||||
|
||||
Object[] r1 = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, colWidths,
|
||||
new String[] { "生产令号", rooteProdet, "开始时间", formatte11r.format(starttime), "结束时间", formatte11r.format(endtime) },
|
||||
rowHeight, minY);
|
||||
yStart = (Float) r1[1];
|
||||
|
||||
Object[] r2 = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart - 2, tableWidth, colWidths,
|
||||
new String[] { "单据编号", "生产订单编号", "产品名称", "产品编码", "数量", "生产车间", "次数" }, rowHeight, minY);
|
||||
yStart = (Float) r2[1];
|
||||
|
||||
Object[] r3 = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart - 2, tableWidth, colWidths, new String[] {
|
||||
productionOrderNumber, combinedVo.getOrderNumber(), combinedVo.getMaterialName(), combinedVo.getMaterialCode(),
|
||||
String.valueOf(fmoQty), fDepartmentName, "1", "" }, rowHeight, minY);
|
||||
yStart = (Float) r3[1];
|
||||
|
||||
float[] processColWidths = scaleWidths(new float[] { 35, 78, 35, 55, 45, 170, 55, 60, 60 }, tableWidth);
|
||||
yStart = yStart - 6;
|
||||
Object[] headerResult = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, processColWidths,
|
||||
new String[] { "工序号", "工序名称", "数量", "工作中心", "工时(分)", "工序说明", "控制码", "开始时间", "结束时间" },
|
||||
rowHeight, minY);
|
||||
yStart = (Float) headerResult[1];
|
||||
|
||||
if (processes != null) {
|
||||
SimpleDateFormat formatte1r = new SimpleDateFormat("yyyy-MM-dd");
|
||||
for (PlannedProcessVo process : processes) {
|
||||
yStart = yStart - rowHeight;
|
||||
Object[] rowResult = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, processColWidths, new String[] {
|
||||
String.valueOf(process.getFOperNumber()), process.getFProcessName(),
|
||||
String.valueOf(process.getFOperQty()),
|
||||
process.getFWorkCenterName(), String.format("%.2f", process.getFActivity1BaseQty()),
|
||||
process.getFOperDescription(),
|
||||
process.getFOptCtrlCodeIFName(),
|
||||
process.getFSeqPlanStartTime() != null ? formatte1r.format(process.getFSeqPlanStartTime()) : "",
|
||||
process.getFSeqPlanFinishTime() != null ? formatte1r.format(process.getFSeqPlanFinishTime()) : ""
|
||||
}, rowHeight, minY);
|
||||
float nextY = (Float) rowResult[1];
|
||||
if (nextY == yStart || nextY <= minY) {
|
||||
break;
|
||||
}
|
||||
yStart = nextY;
|
||||
}
|
||||
}
|
||||
|
||||
yStart = yStart - 15;
|
||||
if (yStart >= minY) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(font, 10);
|
||||
contentStream.newLineAtOffset(margin, yStart);
|
||||
contentStream.showText("生产用料清单:" + materialCodeQr);
|
||||
contentStream.endText();
|
||||
|
||||
PDImageXObject qrCode2 = generateQRCode(document, materialCodeQr, (int) qrSize, (int) qrSize);
|
||||
float qrTopY = yStart + 10;
|
||||
if (qrTopY - qrSize >= minY - 20) {
|
||||
contentStream.drawImage(qrCode2, qrX, qrTopY - qrSize);
|
||||
}
|
||||
yStart = yStart - 12;
|
||||
}
|
||||
|
||||
yStart = yStart - 2;
|
||||
float[] materialColWidths = scaleWidths(new float[] { 25, 130, 110, 50, 35, 40, 40, 80, 50 }, tableWidth);
|
||||
Object[] mHeader = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, materialColWidths, new String[] {
|
||||
"序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注"
|
||||
}, rowHeight, minY);
|
||||
yStart = (Float) mHeader[1];
|
||||
|
||||
List<MaterialUsageDTO> materialUsageDTOList = combinedVo.getMaterialUsageDTOList();
|
||||
if (materialUsageDTOList != null && !materialUsageDTOList.isEmpty()) {
|
||||
for (int i = 0; i < materialUsageDTOList.size(); i++) {
|
||||
MaterialUsageDTO material = materialUsageDTOList.get(i);
|
||||
Object[] mr = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart - 2, tableWidth, materialColWidths, new String[] {
|
||||
String.valueOf(i + 1), material.getMaterialCode(),
|
||||
material.getMaterialName(),
|
||||
material.getSpecification(), material.getUnit(),
|
||||
String.valueOf(material.getRequiredQty()),
|
||||
String.valueOf(material.getPickedQty()),
|
||||
material.getStockName(),
|
||||
material.getRemarks() != null ? material.getRemarks() : ""
|
||||
}, rowHeight, minY);
|
||||
float nextY = (Float) mr[1];
|
||||
if (nextY == yStart - 2 || nextY <= minY) {
|
||||
break;
|
||||
}
|
||||
yStart = nextY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float[] scaleWidths(float[] widths, float targetTotal) {
|
||||
float sum = 0f;
|
||||
for (float w : widths) {
|
||||
sum += w;
|
||||
}
|
||||
if (sum <= 0f || targetTotal <= 0f) {
|
||||
return widths;
|
||||
}
|
||||
float scale = targetTotal / sum;
|
||||
float[] scaled = new float[widths.length];
|
||||
for (int i = 0; i < widths.length; i++) {
|
||||
scaled[i] = widths[i] * scale;
|
||||
}
|
||||
return scaled;
|
||||
}
|
||||
|
||||
private static float drawTableRowNoPageBreak(PDPageContentStream contentStream, PDType0Font font, float fontSize, float margin,
|
||||
float yStart, float tableWidth, float[] colWidths, String[] cells, float rowHeight, float minY) throws IOException {
|
||||
if (yStart - rowHeight < minY) {
|
||||
return yStart;
|
||||
}
|
||||
|
||||
float nextX = margin;
|
||||
float nextY = yStart;
|
||||
int maxColumns = Math.min(cells.length, colWidths.length);
|
||||
|
||||
for (int i = 0; i < maxColumns; i++) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(font, fontSize);
|
||||
contentStream.newLineAtOffset(nextX + 4, nextY - (fontSize + 4));
|
||||
String cellText = cells[i] != null ? cells[i].replace("\n", "") : "";
|
||||
contentStream.showText(cellText);
|
||||
contentStream.endText();
|
||||
|
||||
contentStream.moveTo(nextX, nextY);
|
||||
contentStream.lineTo(nextX, nextY - rowHeight);
|
||||
contentStream.lineTo(nextX + colWidths[i], nextY - rowHeight);
|
||||
contentStream.lineTo(nextX + colWidths[i], nextY);
|
||||
contentStream.closeAndStroke();
|
||||
|
||||
nextX += colWidths[i];
|
||||
}
|
||||
|
||||
return yStart - rowHeight;
|
||||
}
|
||||
|
||||
private static Object[] drawTableRowWithWrapNoPageBreak(PDPageContentStream contentStream, PDType0Font font, float fontSize,
|
||||
float margin, float yStart, float tableWidth, float[] colWidths, String[] cells, float baseRowHeight, float minY)
|
||||
throws IOException {
|
||||
float nextX = margin;
|
||||
float nextY = yStart;
|
||||
int maxColumns = Math.min(cells.length, colWidths.length);
|
||||
|
||||
List<List<String>> allCellLines = new ArrayList<List<String>>();
|
||||
int maxLines = 1;
|
||||
|
||||
for (int i = 0; i < maxColumns; i++) {
|
||||
String cellText = cells[i] != null ? cells[i] : "";
|
||||
List<String> lines = wrapText(cellText, font, fontSize, colWidths[i]);
|
||||
allCellLines.add(lines);
|
||||
maxLines = Math.max(maxLines, lines.size());
|
||||
}
|
||||
|
||||
float actualRowHeight = baseRowHeight * maxLines;
|
||||
if (nextY - actualRowHeight < minY) {
|
||||
return new Object[]{contentStream, yStart};
|
||||
}
|
||||
|
||||
nextX = margin;
|
||||
for (int i = 0; i < maxColumns; i++) {
|
||||
List<String> lines = allCellLines.get(i);
|
||||
|
||||
contentStream.moveTo(nextX, nextY);
|
||||
contentStream.lineTo(nextX, nextY - actualRowHeight);
|
||||
contentStream.lineTo(nextX + colWidths[i], nextY - actualRowHeight);
|
||||
contentStream.lineTo(nextX + colWidths[i], nextY);
|
||||
contentStream.closeAndStroke();
|
||||
|
||||
for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
|
||||
String line = lines.get(lineIndex);
|
||||
if (!line.isEmpty()) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(font, fontSize);
|
||||
float textY = nextY - (fontSize + 4) - (lineIndex * (fontSize + 1));
|
||||
contentStream.newLineAtOffset(nextX + 4, textY);
|
||||
contentStream.showText(line);
|
||||
contentStream.endText();
|
||||
}
|
||||
}
|
||||
|
||||
nextX += colWidths[i];
|
||||
}
|
||||
|
||||
return new Object[]{contentStream, nextY - actualRowHeight};
|
||||
}
|
||||
|
||||
private static String createSinglePdf(CombinedDTO combinedVo, String rooteProdet, String directoryPath, String fontPath) {
|
||||
if (combinedVo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (CombinedDTO combinedVo : combinedVoList) {
|
||||
if (!combinedVoList.isEmpty()) {
|
||||
// 获取物料编码
|
||||
String materialCode1 = combinedVo.getMaterialCode();
|
||||
String fProcessName;
|
||||
String productionOrderNumber;
|
||||
Date starttime;
|
||||
Date endtime;
|
||||
String materialCode;
|
||||
Long fmoQty;
|
||||
String fDepartmentName;
|
||||
|
||||
String OrderNumber = "";
|
||||
// 获取第一序的工序名称
|
||||
List<PlannedProcessVo> processes = combinedVo.getProcesses();
|
||||
if (processes != null && !processes.isEmpty()) {
|
||||
productionOrderNumber = String.valueOf(processes.get(0).getFBillNo());
|
||||
starttime = processes.get(0).getFPlanStartTime();
|
||||
endtime = processes.get(0).getFPlanFinishTime();
|
||||
if (combinedVo.getMaterialUsageDTOList() != null && !combinedVo.getMaterialUsageDTOList().isEmpty()) {
|
||||
materialCode = String.valueOf(combinedVo.getMaterialUsageDTOList().get(0).getBillNumber());
|
||||
} else {
|
||||
materialCode = "0";
|
||||
}
|
||||
fmoQty = processes.get(0).getFMOQty();
|
||||
fDepartmentName = processes.get(0).getFWorkCenterName();
|
||||
if (!processes.isEmpty()) {
|
||||
fProcessName = processes.get(0).getFProcessName();
|
||||
OrderNumber = combinedVo.getOrderNumber();
|
||||
productionOrderNumber = String.valueOf(combinedVo.getProcesses().get(0).getFBillNo());
|
||||
starttime = combinedVo.getProcesses().get(0).getFPlanStartTime();
|
||||
endtime = combinedVo.getProcesses().get(0).getFPlanFinishTime();
|
||||
List<MaterialUsageDTO> usageList = combinedVo.getMaterialUsageDTOList();
|
||||
materialCode = (usageList != null && !usageList.isEmpty() && usageList.get(0) != null)
|
||||
? String.valueOf(usageList.get(0).getBillNumber())
|
||||
: combinedVo.getOrderNumber();
|
||||
fmoQty = combinedVo.getProcesses().get(0).getFMOQty();
|
||||
fDepartmentName = combinedVo.getProcesses().get(0).getFWorkCenterName();
|
||||
// 其他代码...
|
||||
} else {
|
||||
// 处理列表为空的情况
|
||||
fProcessName = "A";
|
||||
productionOrderNumber = "A";
|
||||
starttime = new Date();
|
||||
endtime = new Date();
|
||||
@ -526,146 +188,164 @@ public class PDFGenerator {
|
||||
fDepartmentName = "";
|
||||
}
|
||||
|
||||
String pdfFileName = fDepartmentName + "_" + (materialCode1 != null ? materialCode1.replace("/", "_") : "") + "_独立" + ".pdf";
|
||||
String pdfFileName = fDepartmentName + "_" + materialCode1.replace("/", "_") + "_独立" + ".pdf";
|
||||
|
||||
// 1. 获取文件夹名
|
||||
String departmentFolderName = fDepartmentName;
|
||||
if (departmentFolderName == null || departmentFolderName.trim().isEmpty()) {
|
||||
// 兜底:从文件名取前四个字或以'_'分割第一个元素
|
||||
String[] parts = pdfFileName.split("_");
|
||||
departmentFolderName = parts.length > 0 ? parts[0] : pdfFileName.substring(0, Math.min(4, pdfFileName.length()));
|
||||
departmentFolderName = parts.length > 0 ? parts[0]
|
||||
: pdfFileName.substring(0, Math.min(4, pdfFileName.length()));
|
||||
}
|
||||
|
||||
// 2. 创建文件夹
|
||||
File departmentFolder = new File(directoryPath, departmentFolderName);
|
||||
if (!departmentFolder.exists()) {
|
||||
departmentFolder.mkdirs();
|
||||
}
|
||||
|
||||
String pdfPath = departmentFolder.getAbsolutePath() + "\\" + fDepartmentName + "_" + (materialCode1 != null ? materialCode1.replace("/", "_") : "") + "_" + productionOrderNumber + ".pdf";
|
||||
// 获取当前时间的毫秒时间戳
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String pdfPath = departmentFolder.getAbsolutePath() + "\\" + fDepartmentName + "_" + materialCode1.replace("/", "_") + "_" + productionOrderNumber + ".pdf";
|
||||
PDDocument document = null;
|
||||
try {
|
||||
// 创建 PDF 文件并写入内容
|
||||
document = new PDDocument();
|
||||
PDPage page = new PDPage();
|
||||
document.addPage(page);
|
||||
PDType0Font font;
|
||||
|
||||
try {
|
||||
font = PDType0Font.load(document, new File(fontPath));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Font file not found", e);
|
||||
}
|
||||
PDType0Font font = loadAvailableFont(document, fontCandidates);
|
||||
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page);
|
||||
PdfDrawContext drawCtx = new PdfDrawContext(contentStream, page);
|
||||
|
||||
float margin = 15;
|
||||
float yStart = 740;
|
||||
float rowHeight = 20f;
|
||||
float pageHeight = page.getMediaBox().getHeight();
|
||||
float pageWidth = page.getMediaBox().getWidth();
|
||||
float qrSize = 90f;
|
||||
float qrGap = 5f;
|
||||
float tableWidth = pageWidth - 2 * margin - (qrSize + qrGap);
|
||||
float qrX = pageWidth - margin - qrSize;
|
||||
float tableWidth = page.getMediaBox().getWidth() - 2 * margin;
|
||||
|
||||
PDImageXObject qrCode1 = generateQRCode(document, productionOrderNumber, (int)qrSize, (int)qrSize);
|
||||
contentStream.drawImage(qrCode1, qrX, yStart - qrSize + 25);
|
||||
// 添加标题和右上角的二维码
|
||||
drawCtx.stream.beginText();
|
||||
drawCtx.stream.setFont(font, 14);
|
||||
drawCtx.stream.newLineAtOffset(220, 780);
|
||||
drawCtx.stream.showText("工序计划单");
|
||||
drawCtx.stream.endText();
|
||||
|
||||
PDImageXObject qrCode1 = generateQRCode(document, productionOrderNumber, 90, 90);
|
||||
drawCtx.stream.drawImage(qrCode1, drawCtx.page.getMediaBox().getWidth() - 110, 710);
|
||||
SimpleDateFormat formatte11r = new SimpleDateFormat("yyyy-MM-dd");
|
||||
float[] colWidths = scaleWidths(new float[] { 60, 95, 110, 110, 60, 60, 55 }, tableWidth);
|
||||
contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, colWidths,
|
||||
new String[] { "生产令号", rooteProdet, "开始时间", formatte11r.format(starttime), "结束时间", formatte11r.format(endtime) },
|
||||
document, page, rowHeight, pageHeight);
|
||||
// 绘制生产订单信息表格
|
||||
float[] colWidths = { 60, 95, 110, 110, 60, 60, 55 };
|
||||
drawTableRow(drawCtx, font, margin, yStart, tableWidth, colWidths,
|
||||
new String[] { "生产令号", rooteProdet,
|
||||
"开始时间", formatte11r.format(starttime), "结束时间", formatte11r.format(endtime) },
|
||||
document, rowHeight, pageHeight);
|
||||
|
||||
yStart -= rowHeight;
|
||||
contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, colWidths,
|
||||
new String[] { "单据编号", "生产订单编号", "产品名称", "产品编码", "数量", "生产车间", "次数" }, document, page,
|
||||
drawTableRow(drawCtx, font, margin, yStart, tableWidth, colWidths,
|
||||
new String[] { "单据编号", "生产订单编号", "产品名称", "产品编码", "数量", "生产车间", "次数" }, document,
|
||||
rowHeight, pageHeight);
|
||||
|
||||
yStart -= rowHeight;
|
||||
contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, colWidths, new String[] {
|
||||
productionOrderNumber, combinedVo.getOrderNumber(), combinedVo.getMaterialName(), combinedVo.getMaterialCode(),
|
||||
String.valueOf(fmoQty), fDepartmentName, "1", "" }, document, page, rowHeight, pageHeight);
|
||||
drawTableRow(drawCtx, font, margin, yStart, tableWidth, colWidths, new String[] {
|
||||
productionOrderNumber, combinedVo.getOrderNumber(), combinedVo.getMaterialName(),
|
||||
combinedVo.getMaterialCode(),
|
||||
String.valueOf(fmoQty), fDepartmentName, "1", "" }, document, rowHeight, pageHeight);
|
||||
|
||||
// 绘制工序表格
|
||||
float processTableStartY = yStart - 2 * rowHeight - 10;
|
||||
float[] processColWidths = scaleWidths(new float[] { 35, 78, 35, 55, 45, 170, 55, 60, 60 }, tableWidth);
|
||||
float[] processColWidths = { 35, 78, 35, 55, 45, 170, 55, 60, 60 };
|
||||
yStart = processTableStartY;
|
||||
|
||||
Object[] result = drawTableRowWithWrap(contentStream, font, margin, yStart, tableWidth, processColWidths, new String[] {
|
||||
"工序号", "工序名称", "数量", "工作中心", "工时(分)", "工序说明", "控制码", "开始时间", "结束时间" }, document, page,
|
||||
// 使用支持换行的方法绘制表头
|
||||
yStart = drawTableRowWithWrap(drawCtx, font, margin, yStart, tableWidth, processColWidths, new String[] {
|
||||
"工序号", "工序名称", "数量", "工作中心", "工时(分)", "工序说明", "控制码", "开始时间", "结束时间" }, document,
|
||||
rowHeight, pageHeight);
|
||||
contentStream = (PDPageContentStream) result[0];
|
||||
yStart = (Float) result[1];
|
||||
|
||||
if (processes != null) {
|
||||
for (PlannedProcessVo process : processes) {
|
||||
for (PlannedProcessVo process : combinedVo.getProcesses()) {
|
||||
SimpleDateFormat formatte1r = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Object[] processResult = drawTableRowWithWrap(contentStream, font, margin, yStart, tableWidth, processColWidths, new String[] {
|
||||
// 使用支持换行的方法绘制数据行,特别是工序说明字段
|
||||
yStart = drawTableRowWithWrap(drawCtx, font, margin, yStart, tableWidth, processColWidths, new String[] {
|
||||
String.valueOf(process.getFOperNumber()), process.getFProcessName(),
|
||||
String.valueOf(process.getFOperQty()),
|
||||
process.getFWorkCenterName(), String.format("%.2f", process.getFActivity1BaseQty()),
|
||||
process.getFOperDescription(),
|
||||
process.getFOptCtrlCodeIFName(),
|
||||
process.getFSeqPlanStartTime() != null ? formatte1r.format(process.getFSeqPlanStartTime()) : "",
|
||||
process.getFSeqPlanFinishTime() != null ? formatte1r.format(process.getFSeqPlanFinishTime()) : ""
|
||||
}, document, page, rowHeight, pageHeight);
|
||||
contentStream = (PDPageContentStream) processResult[0];
|
||||
yStart = (Float) processResult[1];
|
||||
}
|
||||
process.getFSeqPlanStartTime() != null
|
||||
? formatte1r.format(process.getFSeqPlanStartTime())
|
||||
: "",
|
||||
process.getFSeqPlanFinishTime() != null
|
||||
? (formatte1r.format(process.getFSeqPlanFinishTime()))
|
||||
: ""
|
||||
}, document, rowHeight, pageHeight);
|
||||
}
|
||||
|
||||
yStart = yStart - 25;
|
||||
// 添加生产用料清单标题
|
||||
yStart = yStart - 2 * rowHeight - 40;
|
||||
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(font, 12);
|
||||
contentStream.newLineAtOffset(margin, yStart);
|
||||
contentStream.showText("生产用料清单:");
|
||||
contentStream.showText(materialCode);
|
||||
contentStream.endText();
|
||||
drawCtx.stream.beginText();
|
||||
drawCtx.stream.setFont(font, 12);
|
||||
drawCtx.stream.newLineAtOffset(margin, yStart);
|
||||
drawCtx.stream.showText("生产用料清单:");
|
||||
drawCtx.stream.showText(materialCode);
|
||||
drawCtx.stream.endText();
|
||||
|
||||
PDImageXObject qrCode2 = generateQRCode(document, materialCode, (int)qrSize, (int)qrSize);
|
||||
float qrTopY = yStart + 10;
|
||||
contentStream.drawImage(qrCode2, qrX, qrTopY - qrSize);
|
||||
// 在用料清单标题右侧添加二维码,与右上角二维码对齐
|
||||
PDImageXObject qrCode2 = generateQRCode(document, materialCode, 90, 90);
|
||||
drawCtx.stream.drawImage(qrCode2, drawCtx.page.getMediaBox().getWidth() - 110, yStart - 31);
|
||||
|
||||
yStart = yStart - 15;
|
||||
yStart -= rowHeight;
|
||||
|
||||
float[] materialColWidths = scaleWidths(new float[] { 25, 130, 110, 50, 35, 40, 40, 80, 50 }, tableWidth);
|
||||
contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, materialColWidths, new String[] {
|
||||
// 用料清单表头
|
||||
float[] materialColWidths = { 25, 130, 110, 50, 35, 40, 40, 80, 50 };
|
||||
drawTableRow(drawCtx, font, margin, yStart, tableWidth, materialColWidths, new String[] {
|
||||
"序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注"
|
||||
}, document, page, rowHeight, pageHeight);
|
||||
}, document, rowHeight, pageHeight);
|
||||
|
||||
List<MaterialUsageDTO> materialUsageDTOList = combinedVo.getMaterialUsageDTOList();
|
||||
if (materialUsageDTOList != null && !materialUsageDTOList.isEmpty()) {
|
||||
for (int i = 0; i < materialUsageDTOList.size(); i++) {
|
||||
yStart -= rowHeight;
|
||||
|
||||
// 如果空间不足,则新建页面
|
||||
if (yStart - rowHeight < margin) {
|
||||
contentStream.close();
|
||||
page = new PDPage();
|
||||
document.addPage(page);
|
||||
contentStream = new PDPageContentStream(document, page);
|
||||
drawCtx.stream.close();
|
||||
PDPage newPage = new PDPage();
|
||||
document.addPage(newPage);
|
||||
drawCtx.stream = new PDPageContentStream(document, newPage);
|
||||
drawCtx.page = newPage;
|
||||
yStart = pageHeight - margin;
|
||||
contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, materialColWidths, new String[] {
|
||||
|
||||
// 绘制用料清单表头
|
||||
drawTableRow(drawCtx, font, margin, yStart, tableWidth, materialColWidths,
|
||||
new String[] {
|
||||
"序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注"
|
||||
}, document, page, rowHeight, pageHeight);
|
||||
}, document, rowHeight, pageHeight);
|
||||
yStart -= rowHeight;
|
||||
}
|
||||
|
||||
MaterialUsageDTO material = materialUsageDTOList.get(i);
|
||||
contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, materialColWidths,
|
||||
SimpleDateFormat formatte1r = new SimpleDateFormat("yyyy-MM-dd");
|
||||
drawTableRow(drawCtx, font, margin, yStart, tableWidth, materialColWidths,
|
||||
new String[] {
|
||||
String.valueOf(i + 1), material.getMaterialCode(),
|
||||
material.getMaterialName(),
|
||||
material.getSpecification(), material.getUnit(),
|
||||
String.valueOf(material.getRequiredQty()),
|
||||
String.valueOf(material.getPickedQty()),
|
||||
|
||||
material.getStockName(),
|
||||
material.getRemarks() != null ? material.getRemarks() : ""
|
||||
}, document, page, rowHeight, pageHeight);
|
||||
}, document, rowHeight, pageHeight);
|
||||
}
|
||||
}
|
||||
|
||||
contentStream.close();
|
||||
drawCtx.stream.close();
|
||||
document.save(pdfPath);
|
||||
return pdfPath;
|
||||
pdfPaths.add(pdfPath);
|
||||
} catch (IOException | WriterException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
throw new RuntimeException("生成PDF失败: " + pdfPath + ", " + e.getMessage(), e);
|
||||
} finally {
|
||||
if (document != null) {
|
||||
try {
|
||||
@ -676,6 +356,28 @@ public class PDFGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pdfPaths.isEmpty()) {
|
||||
throw new RuntimeException("未生成任何PDF文件,请检查工艺数据或字体配置");
|
||||
}
|
||||
List<File> generatedPdfFiles = collectPdfFiles(new File(directoryPath));
|
||||
if (generatedPdfFiles.isEmpty()) {
|
||||
throw new RuntimeException("目录中未找到已生成PDF: " + directoryPath);
|
||||
}
|
||||
// 生成ZIP文件
|
||||
String zipFilePath = directoryPath + "\\" + rooteProdet + "_工序计划单.zip";
|
||||
try {
|
||||
createZipFile(directoryPath, zipFilePath);
|
||||
File zipFile = new File(zipFilePath);
|
||||
if (!zipFile.exists() || zipFile.length() <= 0) {
|
||||
throw new IOException("ZIP文件为空或不存在: " + zipFilePath);
|
||||
}
|
||||
return zipFilePath;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("生成ZIP失败: " + zipFilePath + ", " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建ZIP文件
|
||||
@ -690,36 +392,96 @@ public class PDFGenerator {
|
||||
zipFile.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
Set<String> fileNamesInZip = new HashSet<>();
|
||||
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(Paths.get(zipFilePath)))) {
|
||||
// 设置压缩级别
|
||||
zipOut.setLevel(ZipOutputStream.DEFLATED);
|
||||
|
||||
File sourceDir = new File(sourceDirPath);
|
||||
zipDirectory(sourceDir, sourceDir.getName() + "/", zipOut);
|
||||
Path rootPath = Paths.get(sourceDirPath).toAbsolutePath().normalize();
|
||||
List<File> filesToZip = collectPdfFiles(new File(sourceDirPath));
|
||||
if (filesToZip.isEmpty()) {
|
||||
throw new IOException("目录中未找到可打包PDF: " + sourceDirPath);
|
||||
}
|
||||
Set<String> entryNames = new HashSet<>();
|
||||
for (File file : filesToZip) {
|
||||
Path filePath = file.toPath().toAbsolutePath().normalize();
|
||||
String entryName = rootPath.relativize(filePath).toString().replace("\\", "/");
|
||||
if (entryName.trim().isEmpty() || !entryNames.add(entryName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
private static void zipDirectory(File folder, String parentFolder, ZipOutputStream zipOut) throws IOException {
|
||||
for (File file : Objects.requireNonNull(folder.listFiles())) {
|
||||
if (file.isDirectory()) {
|
||||
zipDirectory(file, parentFolder + file.getName() + "/", zipOut);
|
||||
} else {
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
ZipEntry zipEntry = new ZipEntry(parentFolder + file.getName());
|
||||
ZipEntry zipEntry = new ZipEntry(entryName);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
|
||||
byte[] bytes = new byte[1024];
|
||||
int length;
|
||||
while ((length = fis.read(bytes)) >= 0) {
|
||||
zipOut.write(bytes, 0, length);
|
||||
}
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<File> collectPdfFiles(File root) {
|
||||
List<File> result = new ArrayList<>();
|
||||
if (root == null || !root.exists()) {
|
||||
return result;
|
||||
}
|
||||
if (root.isFile()) {
|
||||
if (root.getName().toLowerCase().endsWith(".pdf")) {
|
||||
result.add(root);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
File[] children = root.listFiles();
|
||||
if (children == null) {
|
||||
return result;
|
||||
}
|
||||
for (File child : children) {
|
||||
result.addAll(collectPdfFiles(child));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PDType0Font loadAvailableFont(PDDocument document, String[] fontCandidates) throws IOException {
|
||||
Exception last = null;
|
||||
StringBuilder triedPaths = new StringBuilder();
|
||||
for (String fontPath : fontCandidates) {
|
||||
if (fontPath == null || fontPath.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (triedPaths.length() > 0) {
|
||||
triedPaths.append(", ");
|
||||
}
|
||||
triedPaths.append(fontPath);
|
||||
try {
|
||||
if (fontPath.startsWith("classpath:")) {
|
||||
String classpathLocation = fontPath.substring("classpath:".length());
|
||||
if (classpathLocation.startsWith("/")) {
|
||||
classpathLocation = classpathLocation.substring(1);
|
||||
}
|
||||
try (InputStream in = PDFGenerator.class.getClassLoader().getResourceAsStream(classpathLocation)) {
|
||||
if (in == null) {
|
||||
continue;
|
||||
}
|
||||
return PDType0Font.load(document, in, true);
|
||||
}
|
||||
} else {
|
||||
File file = new File(fontPath);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
continue;
|
||||
}
|
||||
return PDType0Font.load(document, file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
last = e;
|
||||
}
|
||||
}
|
||||
if (last instanceof IOException) {
|
||||
throw new IOException("字体加载失败,请检查字体文件: " + triedPaths, last);
|
||||
}
|
||||
throw new IOException("未找到可用中文字体,请检查字体文件路径配置: " + triedPaths);
|
||||
}
|
||||
|
||||
// 文件搜索方法:根据物料编码在文件夹中查找文件名包含该编码的PDF文件
|
||||
public static File findFileWithMaterialCode(String directoryPath, String materialCode) {
|
||||
File directory = new File(directoryPath);
|
||||
@ -885,10 +647,11 @@ public class PDFGenerator {
|
||||
}
|
||||
|
||||
// Java 8兼容的改进表格绘制方法,支持自动换行
|
||||
private static Object[] drawTableRowWithWrap(PDPageContentStream contentStream, PDType0Font font, float margin,
|
||||
private static float drawTableRowWithWrap(PdfDrawContext ctx, PDType0Font font, float margin,
|
||||
float yStart, float tableWidth, float[] colWidths, String[] cells, PDDocument document,
|
||||
PDPage page, float baseRowHeight, float pageHeight) throws IOException {
|
||||
float baseRowHeight, float pageHeight) throws IOException {
|
||||
|
||||
PDPageContentStream contentStream = ctx.stream;
|
||||
float nextX = margin;
|
||||
float nextY = yStart;
|
||||
float fontSize = 10;
|
||||
@ -910,10 +673,11 @@ public class PDFGenerator {
|
||||
// 检查是否需要分页
|
||||
if (nextY - actualRowHeight < margin) {
|
||||
contentStream.close();
|
||||
// 创建新页面
|
||||
page = new PDPage();
|
||||
document.addPage(page);
|
||||
contentStream = new PDPageContentStream(document, page);
|
||||
PDPage newPage = new PDPage();
|
||||
document.addPage(newPage);
|
||||
contentStream = new PDPageContentStream(document, newPage);
|
||||
ctx.stream = contentStream;
|
||||
ctx.page = newPage;
|
||||
// 重置Y坐标为新页面顶部
|
||||
nextY = pageHeight - margin;
|
||||
}
|
||||
@ -946,7 +710,7 @@ public class PDFGenerator {
|
||||
nextX += colWidths[i];
|
||||
}
|
||||
|
||||
return new Object[]{contentStream, nextY - actualRowHeight}; // 返回contentStream和下一行的Y坐标
|
||||
return nextY - actualRowHeight; // 返回下一行的Y坐标
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,9 +6,13 @@ import com.ruoyi.system.domain.vo.ImMaterialVo;
|
||||
import com.ruoyi.system.domain.bo.ImMaterialBo;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.BuildCapacityDTO;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialsDTO;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 物料检索Service接口
|
||||
@ -66,4 +70,16 @@ public interface IImMaterialService {
|
||||
Boolean insertJDMaterial(List<ImMaterial> materialList);
|
||||
|
||||
Boolean updateByFMid(List<ImMaterial> imMaterials);
|
||||
|
||||
BuildCapacityDTO queryBuildCapacity(String productionOrderNo, String productCode, String productName);
|
||||
|
||||
List<BuildCapacityDTO> queryBuildCapacityByProductionOrderNo(String productionOrderNo);
|
||||
|
||||
ProductionOrderMaterialsDTO queryProductionOrderMaterials(String productionOrderNo, String productCode);
|
||||
|
||||
/**
|
||||
* 分页扫描 im_material,按物料编码批量解析单重(production_order → process_route)并写回;
|
||||
* 仅解析到单重且与库中值不同时更新 single_weight、modify_date、update_time。
|
||||
*/
|
||||
R<Map<String, Object>> refreshImMaterialSingleWeightFromOrders();
|
||||
}
|
||||
|
||||
@ -99,6 +99,20 @@ public interface IProcessOrderProService {
|
||||
*/
|
||||
void pushDingTalkCard(Long id);
|
||||
|
||||
/**
|
||||
* 生成工艺计划表 Excel(出图场景模板)并发送至钉钉机器人配置的接收人
|
||||
*
|
||||
* @param id 项目令号主键 ID(与列表/详情中的 id 一致)
|
||||
*/
|
||||
R<Void> sendProcessRoutePlanExcel(Long id);
|
||||
|
||||
/**
|
||||
* 查询工艺计划表(钉钉)已成功发送次数,供前端按钮等展示
|
||||
*
|
||||
* @param id 项目令号主键 ID
|
||||
*/
|
||||
R<Integer> getRoutePlanSendCount(Long id);
|
||||
|
||||
/**
|
||||
* 刷新并计算项目延期状态
|
||||
*/
|
||||
|
||||
@ -3,6 +3,8 @@ package com.ruoyi.system.service;
|
||||
import com.ruoyi.system.domain.ProductionOrder;
|
||||
import com.ruoyi.system.domain.bo.FigureSaveBo;
|
||||
import com.ruoyi.system.domain.bo.ProcessOrderProBo;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO;
|
||||
import com.ruoyi.system.domain.vo.ProductionOrderVo;
|
||||
import com.ruoyi.system.domain.bo.ProductionOrderBo;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
@ -10,6 +12,7 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 生产订单Service接口
|
||||
@ -62,4 +65,25 @@ public interface IProductionOrderService {
|
||||
* 是否属于外购件
|
||||
*/
|
||||
Boolean isPurchas(String proCode,String materialCode);
|
||||
|
||||
/**
|
||||
* 查询生产令号列表(去重,用于下拉选择)
|
||||
*/
|
||||
List<String> selectProductionOrderNoOptions(String keyword, Integer limit);
|
||||
|
||||
/**
|
||||
* 通过物料编码匹配系统单重(未匹配数据会跳过)
|
||||
*/
|
||||
List<ProductionOrderWeightExportDTO> matchSingleWeightByMaterialCode(List<ProductionOrderWeightImportDTO> rows);
|
||||
|
||||
/**
|
||||
* 按物料图号/编码解析单重:先 production_order.drawing_no(最新一条且 single_weight 非空),再 process_route.material_code 的 disc_weight。
|
||||
* 未查到返回 null。
|
||||
*/
|
||||
Double resolveSingleWeightByMaterialCode(String materialCode);
|
||||
|
||||
/**
|
||||
* 批量解析单重(内部 IN 分批),仅返回能查到单重的编码;查不到的编码不出现在 Map 中。
|
||||
*/
|
||||
Map<String, Double> resolveSingleWeightByMaterialCodes(Collection<String> materialCodes);
|
||||
}
|
||||
|
||||
@ -1,16 +1,26 @@
|
||||
package com.ruoyi.system.service;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.bo.KingdeeOrderTraceQueryBo;
|
||||
import com.ruoyi.system.domain.bo.KingdeeWorkCenterDataBo;
|
||||
import com.ruoyi.system.domain.vo.KingdeeMoTraceVo;
|
||||
import com.ruoyi.system.domain.vo.KingdeeOrderTraceVo;
|
||||
import com.ruoyi.system.domain.vo.KingdeePoTraceVo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class MssqlQueryService {
|
||||
@ -26,6 +36,208 @@ public class MssqlQueryService {
|
||||
return jdbcTemplate.queryForList(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按生产令号、采购单号、生产订单号、产品编码(均可选,至少填一项)查询金蝶采购与生产追溯数据。
|
||||
*/
|
||||
@DS("sqlserver")
|
||||
public KingdeeOrderTraceVo queryOrderTrace(KingdeeOrderTraceQueryBo bo) {
|
||||
if (bo == null) {
|
||||
bo = new KingdeeOrderTraceQueryBo();
|
||||
}
|
||||
List<String> sclhList = normalizeSclhList(bo.getSclh());
|
||||
String purchaseBillNo = blankToNull(bo.getPurchaseBillNo());
|
||||
String moBillNo = blankToNull(bo.getMoBillNo());
|
||||
String materialNumber = blankToNull(bo.getMaterialNumber());
|
||||
if (sclhList.isEmpty() && purchaseBillNo == null && moBillNo == null && materialNumber == null) {
|
||||
throw new ServiceException("请至少填写生产令号、采购订单、生产订单或产品编码之一");
|
||||
}
|
||||
|
||||
KingdeeOrderTraceVo vo = new KingdeeOrderTraceVo();
|
||||
vo.setPurchaseList(queryPurchaseTrace(sclhList, purchaseBillNo, materialNumber));
|
||||
vo.setProductionList(queryProductionTrace(sclhList, moBillNo, materialNumber));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private List<KingdeePoTraceVo> queryPurchaseTrace(List<String> sclhList, String purchaseBillNo, String materialNumber) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT ");
|
||||
sql.append("PO.F_UCHN_TEXT2 AS sclh, PO.FBILLNO AS purchaseBillNo, PO.FDATE AS purchaseDate, ");
|
||||
sql.append("MAT.FNUMBER AS materialNumber, MAT_L.FNAME AS materialName, PO_E.FQty AS purchaseQty, ");
|
||||
sql.append("INSTOCK.FBILLNO AS instockBillNo, INSTOCK.FDATE AS instockDate, INSTOCK_E.FREALQTY AS instockQty ");
|
||||
sql.append("FROM dbo.T_PUR_POORDER PO ");
|
||||
sql.append("INNER JOIN dbo.T_PUR_POORDERENTRY PO_E ON PO.FID = PO_E.FID ");
|
||||
sql.append("LEFT JOIN dbo.T_BD_MATERIAL MAT ON PO_E.FMATERIALID = MAT.FMATERIALID ");
|
||||
sql.append("LEFT JOIN dbo.T_BD_MATERIAL_L MAT_L ON MAT.FMATERIALID = MAT_L.FMATERIALID AND MAT_L.FLOCALEID = 2052 ");
|
||||
sql.append("LEFT JOIN dbo.T_STK_INSTOCKENTRY INSTOCK_E ON PO_E.FENTRYID = INSTOCK_E.FPOORDERENTRYID ");
|
||||
sql.append("LEFT JOIN dbo.T_STK_INSTOCK INSTOCK ON INSTOCK_E.FID = INSTOCK.FID ");
|
||||
sql.append("WHERE 1 = 1 ");
|
||||
List<Object> args = new ArrayList<>();
|
||||
appendIn(sql, args, "PO.F_UCHN_TEXT2", sclhList);
|
||||
appendEq(sql, args, "PO.FBILLNO", purchaseBillNo);
|
||||
appendEq(sql, args, "MAT.FNUMBER", materialNumber);
|
||||
sql.append("ORDER BY PO.FDATE DESC");
|
||||
return mapPoRows(jdbcTemplate.queryForList(sql.toString(), args.toArray()));
|
||||
}
|
||||
|
||||
private List<KingdeeMoTraceVo> queryProductionTrace(List<String> sclhList, String moBillNo, String materialNumber) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT ");
|
||||
sql.append("MO.F_HBYT_SCLH AS sclh, MO.FBILLNO AS moBillNo, MAT.FNUMBER AS materialNumber, MAT_L.FNAME AS materialName, ");
|
||||
sql.append("MO_E.FQty AS qty, MO.FDATE AS orderDate, OP.FBILLNO AS operPlanningBillNo, OP.FID AS operPlanningId, ");
|
||||
sql.append("MO_E.FPlanStartDate AS planStartDate, MO_E.FPlanFinishDate AS planFinishDate, ");
|
||||
sql.append("INSTOCK.FBILLNO AS instockBillNo, INSTOCK.FDATE AS instockDate, INSTOCK_E.FREALQTY AS instockQty ");
|
||||
sql.append("FROM dbo.T_PRD_MO MO ");
|
||||
sql.append("INNER JOIN dbo.T_PRD_MOENTRY MO_E ON MO.FID = MO_E.FID ");
|
||||
sql.append("LEFT JOIN dbo.T_BD_MATERIAL MAT ON MO_E.FMATERIALID = MAT.FMATERIALID ");
|
||||
sql.append("LEFT JOIN dbo.T_BD_MATERIAL_L MAT_L ON MAT.FMATERIALID = MAT_L.FMATERIALID AND MAT_L.FLOCALEID = 2052 ");
|
||||
sql.append("LEFT JOIN dbo.T_SFC_OPERPLANNING OP ON MO.FID = OP.FMOID AND MO_E.FENTRYID = OP.FMOENTRYID ");
|
||||
sql.append("LEFT JOIN dbo.T_PRD_INSTOCKENTRY INSTOCK_E ON MO.FID = INSTOCK_E.FMOID AND MO_E.FENTRYID = INSTOCK_E.FMOENTRYID ");
|
||||
sql.append("LEFT JOIN dbo.T_PRD_INSTOCK INSTOCK ON INSTOCK_E.FID = INSTOCK.FID ");
|
||||
sql.append("WHERE 1 = 1 ");
|
||||
List<Object> args = new ArrayList<>();
|
||||
appendIn(sql, args, "MO.F_HBYT_SCLH", sclhList);
|
||||
appendEq(sql, args, "MO.FBILLNO", moBillNo);
|
||||
appendEq(sql, args, "MAT.FNUMBER", materialNumber);
|
||||
sql.append("ORDER BY MO.FDATE DESC");
|
||||
return mapMoRows(jdbcTemplate.queryForList(sql.toString(), args.toArray()));
|
||||
}
|
||||
|
||||
private static void appendEq(StringBuilder sql, List<Object> args, String column, String value) {
|
||||
if (value != null) {
|
||||
sql.append(" AND ").append(column).append(" = ? ");
|
||||
args.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生产令号 IN 查询;列表为空则不追加条件。
|
||||
*/
|
||||
private static void appendIn(StringBuilder sql, List<Object> args, String column, List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sql.append(" AND ").append(column).append(" IN (");
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
if (i > 0) {
|
||||
sql.append(", ");
|
||||
}
|
||||
sql.append("?");
|
||||
args.add(values.get(i));
|
||||
}
|
||||
sql.append(") ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并多个传参及逗号分隔项,去重保序。
|
||||
*/
|
||||
private static List<String> normalizeSclhList(List<String> raw) {
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
for (String s : raw) {
|
||||
if (StringUtils.isBlank(s)) {
|
||||
continue;
|
||||
}
|
||||
String t = s.trim();
|
||||
if (t.contains(",")) {
|
||||
for (String part : t.split(",")) {
|
||||
String p = part.trim();
|
||||
if (StringUtils.isNotBlank(p)) {
|
||||
seen.add(p);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seen.add(t);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(seen);
|
||||
}
|
||||
|
||||
private static String blankToNull(String s) {
|
||||
if (StringUtils.isBlank(s)) {
|
||||
return null;
|
||||
}
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
private List<KingdeePoTraceVo> mapPoRows(List<Map<String, Object>> rows) {
|
||||
List<KingdeePoTraceVo> list = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
KingdeePoTraceVo v = new KingdeePoTraceVo();
|
||||
v.setSclh(getString(row, "sclh"));
|
||||
v.setPurchaseBillNo(getString(row, "purchaseBillNo"));
|
||||
v.setPurchaseDate(toUtilDate(getCell(row, "purchaseDate")));
|
||||
v.setMaterialNumber(getString(row, "materialNumber"));
|
||||
v.setMaterialName(getString(row, "materialName"));
|
||||
v.setPurchaseQty(getBigDecimal(row, "purchaseQty"));
|
||||
v.setInstockBillNo(getString(row, "instockBillNo"));
|
||||
v.setInstockDate(toUtilDate(getCell(row, "instockDate")));
|
||||
v.setInstockQty(getBigDecimal(row, "instockQty"));
|
||||
list.add(v);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<KingdeeMoTraceVo> mapMoRows(List<Map<String, Object>> rows) {
|
||||
List<KingdeeMoTraceVo> list = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
KingdeeMoTraceVo v = new KingdeeMoTraceVo();
|
||||
v.setSclh(getString(row, "sclh"));
|
||||
v.setMoBillNo(getString(row, "moBillNo"));
|
||||
v.setMaterialNumber(getString(row, "materialNumber"));
|
||||
v.setMaterialName(getString(row, "materialName"));
|
||||
v.setQty(getBigDecimal(row, "qty"));
|
||||
v.setOrderDate(toUtilDate(getCell(row, "orderDate")));
|
||||
v.setOperPlanningBillNo(getString(row, "operPlanningBillNo"));
|
||||
v.setOperPlanningId(getLong(row, "operPlanningId"));
|
||||
v.setPlanStartDate(toUtilDate(getCell(row, "planStartDate")));
|
||||
v.setPlanFinishDate(toUtilDate(getCell(row, "planFinishDate")));
|
||||
v.setInstockBillNo(getString(row, "instockBillNo"));
|
||||
v.setInstockDate(toUtilDate(getCell(row, "instockDate")));
|
||||
v.setInstockQty(getBigDecimal(row, "instockQty"));
|
||||
list.add(v);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private Object getCell(Map<String, Object> row, String key) {
|
||||
if (row.containsKey(key)) {
|
||||
return row.get(key);
|
||||
}
|
||||
for (Map.Entry<String, Object> e : row.entrySet()) {
|
||||
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Date toUtilDate(Object val) {
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
if (val instanceof Date) {
|
||||
return (Date) val;
|
||||
}
|
||||
if (val instanceof Timestamp) {
|
||||
return new Date(((Timestamp) val).getTime());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long getLong(Map<String, Object> row, String key) {
|
||||
Object val = getCell(row, key);
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(val.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据物料编码查询是否是VMI业务
|
||||
* @param materialNumber 物料编码
|
||||
@ -147,13 +359,13 @@ public class MssqlQueryService {
|
||||
}
|
||||
|
||||
private String getString(Map<String, Object> row, String key) {
|
||||
Object val = row.get(key);
|
||||
Object val = getCell(row, key);
|
||||
return val != null ? val.toString() : "";
|
||||
}
|
||||
|
||||
|
||||
private BigDecimal getBigDecimal(Map<String, Object> row, String key) {
|
||||
Object val = row.get(key);
|
||||
Object val = getCell(row, key);
|
||||
if (val == null) return BigDecimal.ZERO;
|
||||
try {
|
||||
return new BigDecimal(val.toString()).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
@ -16,6 +16,7 @@ import com.ruoyi.common.core.domain.model.XcxLoginUser;
|
||||
import com.ruoyi.common.enums.DeviceType;
|
||||
import com.ruoyi.common.enums.LoginType;
|
||||
import com.ruoyi.common.enums.UserStatus;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.exception.user.CaptchaException;
|
||||
import com.ruoyi.common.exception.user.CaptchaExpireException;
|
||||
import com.ruoyi.common.exception.user.UserException;
|
||||
@ -34,6 +35,8 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
@ -56,6 +59,15 @@ public class SysLoginService {
|
||||
@Value("${user.password.lockTime}")
|
||||
private Integer lockTime;
|
||||
|
||||
@Value("${plm.auth.app-key:}")
|
||||
private String plmAuthAppKey;
|
||||
|
||||
@Value("${plm.auth.app-secret:}")
|
||||
private String plmAuthAppSecret;
|
||||
|
||||
@Value("${plm.auth.username:}")
|
||||
private String plmAuthUsername;
|
||||
|
||||
/**
|
||||
* 登录验证
|
||||
*
|
||||
@ -136,6 +148,27 @@ public class SysLoginService {
|
||||
return StpUtil.getTokenValue();
|
||||
}
|
||||
|
||||
public String plmToken(String appKey, String appSecret) {
|
||||
if (StringUtils.isBlank(plmAuthAppKey) || StringUtils.isBlank(plmAuthAppSecret)) {
|
||||
throw new ServiceException("PLM鉴权未配置");
|
||||
}
|
||||
if (!safeEquals(plmAuthAppKey, StringUtils.trimToEmpty(appKey))
|
||||
|| !safeEquals(plmAuthAppSecret, StringUtils.trimToEmpty(appSecret))) {
|
||||
recordLogininfor("plm", Constants.LOGIN_FAIL, "plm auth failed");
|
||||
throw new ServiceException("PLM鉴权失败");
|
||||
}
|
||||
if (StringUtils.isBlank(plmAuthUsername)) {
|
||||
throw new ServiceException("PLM鉴权用户未配置");
|
||||
}
|
||||
|
||||
SysUser user = loadUserByUsername(plmAuthUsername);
|
||||
LoginUser loginUser = buildLoginUser(user);
|
||||
LoginHelper.loginByDevice(loginUser, DeviceType.PC);
|
||||
recordLogininfor(user.getUserName(), Constants.LOGIN_SUCCESS, "plm token issued");
|
||||
recordLoginInfo(user.getUserId(), user.getUserName());
|
||||
return StpUtil.getTokenValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
@ -330,4 +363,11 @@ public class SysLoginService {
|
||||
// 登录成功 清空错误次数
|
||||
RedisUtils.deleteObject(errorKey);
|
||||
}
|
||||
|
||||
private static boolean safeEquals(String a, String b) {
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,7 +218,7 @@ public class BomDetailsServiceImpl implements IBomDetailsService {
|
||||
));
|
||||
|
||||
for (BomDetails bomDetail : bomDetails) {
|
||||
String key = bomDetail.getPartNumber() + "-" + bomDetail.getName();
|
||||
String key = bomDetail.getPartNumber() + "-" + bomDetail.getName()+System.currentTimeMillis();
|
||||
MaterialBom matchedMaterialBom = materialBomMap.get(key);
|
||||
if (matchedMaterialBom != null) {
|
||||
if (matchedMaterialBom.getUnit().equals("根")) {
|
||||
|
||||
@ -15,23 +15,31 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.kingdee.bos.webapi.sdk.K3CloudApi;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.utils.BeanCopyUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.controller.ImMaterialController;
|
||||
import com.ruoyi.system.domain.ImMaterial;
|
||||
import com.ruoyi.system.domain.ProductionOrder;
|
||||
import com.ruoyi.system.domain.bo.ImMaterialBo;
|
||||
import com.ruoyi.system.domain.dto.JDInventoryDTO;
|
||||
import com.ruoyi.system.domain.dto.JDProductionDTO;
|
||||
import com.ruoyi.system.domain.dto.ProMoDTO;
|
||||
import com.ruoyi.system.domain.dto.PurchaseOrderDTO;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.BuildCapacityDTO;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.CalcInfo;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.MaterialItem;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.StockInfo;
|
||||
import com.ruoyi.system.domain.dto.buildcapacity.StockStatus;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialItemDTO;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialsDTO;
|
||||
import com.ruoyi.system.domain.vo.ImMaterialVo;
|
||||
import com.ruoyi.system.mapper.ImMaterialMapper;
|
||||
import com.ruoyi.system.runner.JdUtil;
|
||||
import com.ruoyi.system.runner.MaterialProperties;
|
||||
import com.ruoyi.system.service.IImMaterialService;
|
||||
import com.ruoyi.system.service.IProductionOrderService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.slf4j.Logger;
|
||||
@ -42,6 +50,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
@ -60,6 +69,7 @@ import static com.ruoyi.common.core.mapper.BaseMapperPlus.log;
|
||||
public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
|
||||
private final ImMaterialMapper baseMapper;
|
||||
private final IProductionOrderService productionOrderService;
|
||||
private static final Logger logger = LoggerFactory.getLogger(ImMaterialController.class);
|
||||
|
||||
@Autowired
|
||||
@ -878,4 +888,300 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BuildCapacityDTO queryBuildCapacity(String productionOrderNo, String productCode, String productName) {
|
||||
// 齐套查询:不查金蝶 BOM,仅使用本地 production_order 中的物料行作为“清单来源”
|
||||
BuildCapacityDTO result = new BuildCapacityDTO();
|
||||
result.setProductCode(productCode);
|
||||
result.setProductName(productName);
|
||||
result.setSelfMadeMaterials(new ArrayList<>());
|
||||
result.setPurchaseMaterials(new ArrayList<>());
|
||||
result.setMaxBuildQty(0);
|
||||
if (StringUtils.isBlank(productionOrderNo) || StringUtils.isBlank(productCode)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
String proNo = productionOrderNo.trim();
|
||||
String proCode = productCode.trim();
|
||||
// 生产令号下的所有行(包含成品行/子项行)
|
||||
List<ProductionOrder> productionOrderList = productionOrderService.selectByProCode(proNo);
|
||||
if (CollectionUtils.isEmpty(productionOrderList)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(result.getProductName())) {
|
||||
// 未传 productName 时,从本地数据中回填成品名称
|
||||
result.setProductName(productionOrderList.stream()
|
||||
.filter(p -> p != null && StringUtils.isNotBlank(p.getDrawingNo()) && proCode.equals(p.getDrawingNo().trim()))
|
||||
.map(ProductionOrder::getDrawingName)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
}
|
||||
|
||||
// 逐行计算:productionOrderList 有多少条,就计算多少条(不做父子级过滤/不做按编码合并)
|
||||
List<MaterialItem> allItems = new ArrayList<>();
|
||||
int minCanSupport = Integer.MAX_VALUE;
|
||||
|
||||
for (ProductionOrder p : productionOrderList) {
|
||||
if (p == null || StringUtils.isBlank(p.getDrawingNo())) {
|
||||
continue;
|
||||
}
|
||||
String materialCode = p.getDrawingNo().trim();
|
||||
int requiredQty = normalizeQtyToInt(p.getQuantity());
|
||||
if (requiredQty <= 0) {
|
||||
requiredQty = 1;
|
||||
}
|
||||
|
||||
int onHandQty = normalizeToInt(Optional.ofNullable(JdUtil.getKuCun(materialCode))
|
||||
.map(list -> list.stream().mapToDouble(JDInventoryDTO::getFBaseQty).sum())
|
||||
.orElse(0.0));
|
||||
int moNotPickQty = normalizeToInt(Optional.ofNullable(JdUtil.getWeiLingliao(materialCode))
|
||||
.map(list -> list.stream().mapToDouble(JDProductionDTO::getFNoPickedQty).sum())
|
||||
.orElse(0.0));
|
||||
int moNotInQty = normalizeToInt(Optional.ofNullable(JdUtil.getProMoList(materialCode))
|
||||
.map(list -> list.stream().mapToDouble(ProMoDTO::getFQty).sum())
|
||||
.orElse(0.0));
|
||||
int poNotInQty = normalizeToInt(Optional.ofNullable(JdUtil.getPurchaseOrderList(materialCode))
|
||||
.map(list -> list.stream().mapToDouble(PurchaseOrderDTO::getFRemainStockINQty).sum())
|
||||
.orElse(0.0));
|
||||
|
||||
int availableQty = onHandQty - moNotPickQty;
|
||||
int finalAvailableQty = availableQty + poNotInQty + moNotInQty;
|
||||
int canSupportQty = Math.max(0, finalAvailableQty / requiredQty);
|
||||
|
||||
StockInfo stock = new StockInfo();
|
||||
stock.setOnHandQty(onHandQty);
|
||||
stock.setMoNotPickQty(moNotPickQty);
|
||||
stock.setMoNotInQty(moNotInQty);
|
||||
stock.setPoNotInQty(poNotInQty);
|
||||
|
||||
CalcInfo calc = new CalcInfo();
|
||||
calc.setAvailableQty(availableQty);
|
||||
calc.setFinalAvailableQty(finalAvailableQty);
|
||||
calc.setCanSupportQty(canSupportQty);
|
||||
|
||||
MaterialItem item = new MaterialItem();
|
||||
item.setMaterialCode(materialCode);
|
||||
item.setMaterialName(p.getDrawingName());
|
||||
item.setRequiredQty(requiredQty);
|
||||
item.setMaterial(p.getMaterial());
|
||||
item.setSingleWeight(p.getSingleWeight());
|
||||
item.setTotalWeight(p.getTotalWeight());
|
||||
item.setRemark(p.getRemark());
|
||||
item.setBatchQuantity(p.getBatchQuantity());
|
||||
item.setStock(stock);
|
||||
item.setCalc(calc);
|
||||
item.setStatus(finalAvailableQty >= requiredQty ? StockStatus.OK : StockStatus.SHORTAGE);
|
||||
allItems.add(item);
|
||||
|
||||
minCanSupport = Math.min(minCanSupport, canSupportQty);
|
||||
}
|
||||
|
||||
if (minCanSupport == Integer.MAX_VALUE) {
|
||||
minCanSupport = 0;
|
||||
}
|
||||
|
||||
List<MaterialItem> purchaseMaterials = new ArrayList<>();
|
||||
List<MaterialItem> selfMadeMaterials = new ArrayList<>();
|
||||
for (MaterialItem item : allItems) {
|
||||
String code = item.getMaterialCode();
|
||||
if (StringUtils.isNotBlank(code) && code.startsWith("009")) {
|
||||
purchaseMaterials.add(item);
|
||||
} else {
|
||||
selfMadeMaterials.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
result.setPurchaseMaterials(purchaseMaterials);
|
||||
result.setSelfMadeMaterials(selfMadeMaterials);
|
||||
result.setMaxBuildQty(minCanSupport);
|
||||
return result;
|
||||
}
|
||||
@Override
|
||||
public List<BuildCapacityDTO> queryBuildCapacityByProductionOrderNo(String productionOrderNo) {
|
||||
if (StringUtils.isBlank(productionOrderNo)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String proNo = productionOrderNo.trim();
|
||||
List<ProductionOrder> productionOrderList = productionOrderService.selectByProCode(proNo);
|
||||
if (CollectionUtils.isEmpty(productionOrderList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<ProductionOrder> topLevel = productionOrderList.stream()
|
||||
.filter(p -> StringUtils.isBlank(p.getParentDrawingNo()))
|
||||
.collect(Collectors.toList());
|
||||
if (CollectionUtils.isEmpty(topLevel)) {
|
||||
topLevel = productionOrderList;
|
||||
}
|
||||
|
||||
Map<String, ProductionOrder> productMap = new LinkedHashMap<>();
|
||||
for (ProductionOrder p : topLevel) {
|
||||
if (p == null || StringUtils.isBlank(p.getDrawingNo())) {
|
||||
continue;
|
||||
}
|
||||
productMap.putIfAbsent(p.getDrawingNo().trim(), p);
|
||||
}
|
||||
|
||||
List<BuildCapacityDTO> list = new ArrayList<>();
|
||||
for (ProductionOrder p : productMap.values()) {
|
||||
list.add(queryBuildCapacity(proNo, p.getDrawingNo(), p.getDrawingName()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static int normalizeQtyToInt(Double value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
if (value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.ceil(value);
|
||||
}
|
||||
|
||||
private static int safeInt(Integer v) {
|
||||
return v == null ? 0 : v;
|
||||
}
|
||||
|
||||
private static double safeDouble(Double v) {
|
||||
return v == null ? 0D : v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProductionOrderMaterialsDTO queryProductionOrderMaterials(String productionOrderNo, String productCode) {
|
||||
ProductionOrderMaterialsDTO dto = new ProductionOrderMaterialsDTO();
|
||||
dto.setProductionOrderNo(productionOrderNo);
|
||||
dto.setProductCode(productCode);
|
||||
dto.setMaterials(Collections.emptyList());
|
||||
if (StringUtils.isBlank(productionOrderNo) || StringUtils.isBlank(productCode)) {
|
||||
return dto;
|
||||
}
|
||||
|
||||
String proNo = productionOrderNo.trim();
|
||||
String proCode = productCode.trim();
|
||||
List<ProductionOrder> productionOrderList = productionOrderService.selectByProCode(proNo);
|
||||
if (CollectionUtils.isEmpty(productionOrderList)) {
|
||||
return dto;
|
||||
}
|
||||
|
||||
dto.setProductName(productionOrderList.stream()
|
||||
.filter(p -> p != null && StringUtils.isNotBlank(p.getDrawingNo()) && proCode.equals(p.getDrawingNo().trim()))
|
||||
.map(ProductionOrder::getDrawingName)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
|
||||
List<ProductionOrderMaterialItemDTO> materials = productionOrderList.stream()
|
||||
.filter(p -> p != null)
|
||||
.filter(p -> StringUtils.isNotBlank(p.getParentDrawingNo()) && proCode.equals(p.getParentDrawingNo().trim()))
|
||||
.map(p -> {
|
||||
ProductionOrderMaterialItemDTO item = new ProductionOrderMaterialItemDTO();
|
||||
item.setMaterialCode(p.getDrawingNo());
|
||||
item.setMaterialName(p.getDrawingName());
|
||||
item.setQty(p.getQuantity());
|
||||
item.setMaterial(p.getMaterial());
|
||||
item.setSingleWeight(p.getSingleWeight());
|
||||
item.setTotalWeight(p.getTotalWeight());
|
||||
item.setRemark(p.getRemark());
|
||||
item.setParentDrawingNo(p.getParentDrawingNo());
|
||||
item.setParentPart(p.getParentPart());
|
||||
item.setBatchQuantity(p.getBatchQuantity());
|
||||
return item;
|
||||
})
|
||||
.sorted(Comparator.comparing(ProductionOrderMaterialItemDTO::getMaterialCode, Comparator.nullsLast(String::compareTo)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
dto.setMaterials(materials);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Map<String, Object>> refreshImMaterialSingleWeightFromOrders() {
|
||||
final int pageSize = 1000;
|
||||
long scanned = 0;
|
||||
long updated = 0;
|
||||
long unchanged = 0;
|
||||
long noResolved = 0;
|
||||
int pageNum = 1;
|
||||
LambdaQueryWrapper<ImMaterial> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.isNotNull(ImMaterial::getMaterialCode);
|
||||
wrapper.ne(ImMaterial::getMaterialCode, "");
|
||||
wrapper.orderByAsc(ImMaterial::getId);
|
||||
|
||||
Date now = new Date();
|
||||
String modifyDay = new SimpleDateFormat("yyyy-MM-dd").format(now);
|
||||
|
||||
while (true) {
|
||||
Page<ImMaterial> pageResult = baseMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
|
||||
List<ImMaterial> records = pageResult.getRecords();
|
||||
if (records.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
scanned += records.size();
|
||||
List<String> codes = records.stream()
|
||||
.map(ImMaterial::getMaterialCode)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<String, Double> weightMap = productionOrderService.resolveSingleWeightByMaterialCodes(codes);
|
||||
for (ImMaterial row : records) {
|
||||
if (StringUtils.isBlank(row.getMaterialCode())) {
|
||||
continue;
|
||||
}
|
||||
String code = row.getMaterialCode().trim();
|
||||
Double resolved = weightMap.get(code);
|
||||
if (resolved == null) {
|
||||
noResolved++;
|
||||
continue;
|
||||
}
|
||||
BigDecimal newBd = BigDecimal.valueOf(resolved).stripTrailingZeros();
|
||||
if (singleWeightUnchanged(row.getSingleWeight(), newBd)) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
row.setSingleWeight(newBd);
|
||||
row.setUpdateTime(now);
|
||||
row.setModifyDate(modifyDay);
|
||||
baseMapper.updateById(row);
|
||||
updated++;
|
||||
}
|
||||
if (records.size() < pageSize) {
|
||||
break;
|
||||
}
|
||||
pageNum++;
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("scanned", scanned);
|
||||
data.put("updated", updated);
|
||||
data.put("unchanged", unchanged);
|
||||
data.put("noResolvedWeight", noResolved);
|
||||
return R.ok("同步完成", data);
|
||||
}
|
||||
|
||||
private static boolean singleWeightUnchanged(BigDecimal oldBd, BigDecimal newBd) {
|
||||
if (oldBd == null && newBd == null) {
|
||||
return true;
|
||||
}
|
||||
if (oldBd == null || newBd == null) {
|
||||
return false;
|
||||
}
|
||||
return oldBd.stripTrailingZeros().compareTo(newBd.stripTrailingZeros()) == 0;
|
||||
}
|
||||
|
||||
private static double toRequiredQty(Double fenzi, Integer fenmu) {
|
||||
double numerator = fenzi == null ? 0D : fenzi;
|
||||
int denominator = fenmu == null || fenmu == 0 ? 1 : fenmu;
|
||||
return numerator / denominator;
|
||||
}
|
||||
|
||||
private static int normalizeToInt(double value) {
|
||||
if (value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.floor(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -41,6 +41,7 @@ import org.apache.commons.collections4.ListUtils;
|
||||
import org.aspectj.bridge.MessageUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@ -86,6 +87,8 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
private final ProcessInfoMapper processInfoMapper;
|
||||
private final ProductionRouteTwoMapper productionRouteTwoMapper;
|
||||
private final ProcessOrderProMapper processOrderProMapper;
|
||||
@Value("${pdf.font-path:classpath:font/arial unicode ms.ttf}")
|
||||
private String pdfFontPath;
|
||||
|
||||
public static JdHuoZhu getFIDToProductionOrder(PlanOrderVo planOrder) {
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
@ -816,8 +819,8 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
for (ProductionOrderVo productionOrderVo : productionOrderVos) {
|
||||
BomDetailsVo bomDetails = new BomDetailsVo();
|
||||
bomDetails.setTotalWeight(productionOrderVo.getProductionOrderNo());
|
||||
bomDetails.setFNumber(productionOrderVo.getMainProducts());
|
||||
bomDetails.setFName(productionOrderVo.getMainProductsName());
|
||||
bomDetails.setFNumber(productionOrderVo.getMainPart());
|
||||
bomDetails.setFName(productionOrderVo.getMainName());
|
||||
bomDetails.setPartdiagramCode(productionOrderVo.getParentDrawingNo());
|
||||
bomDetails.setPartdiagramName(productionOrderVo.getParentPart());
|
||||
bomDetails.setPartNumber(productionOrderVo.getDrawingNo());
|
||||
@ -1444,7 +1447,13 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
@Override
|
||||
public String generatePDFs(String rooteProdet) throws IOException {
|
||||
List<CombinedDTO> selecPlanRouteList = getSelecPlanRouteList(rooteProdet);
|
||||
String pathZIP = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet);
|
||||
if (selecPlanRouteList == null || selecPlanRouteList.isEmpty()) {
|
||||
throw new ServiceException("未查询到可生成PDF的工艺数据");
|
||||
}
|
||||
String pathZIP = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet, Collections.singletonList(pdfFontPath));
|
||||
if (StringUtils.isBlank(pathZIP)) {
|
||||
throw new ServiceException("PDF压缩包生成失败");
|
||||
}
|
||||
return pathZIP;
|
||||
}
|
||||
|
||||
@ -1494,7 +1503,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
public List<PlanOrderVo> getSelectProceOrder1(String rooteProdet) {
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
// 第一次查询:使用 F_HBYT_SCLH
|
||||
List<PlanOrderVo> result = queryWithFieldName(client, "F_HBYT_SCLH", rooteProdet);
|
||||
List<PlanOrderVo> result = queryWithFieldName(client, "F_HBYT_XMH", rooteProdet);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -1504,7 +1513,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
private List<PlanOrderVo> queryWithFieldName(K3CloudApi client, String fieldName, String rooteProdet) {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("FormId", "PRD_MO");
|
||||
json.addProperty("FieldKeys", "F_HBYT_SCLH,FBillNo ,FMaterialId.FNumber,FMaterialName, F_UCHN_BaseProperty_qtr");
|
||||
json.addProperty("FieldKeys", "F_HBYT_XMH,F_HBYT_SCLH,FBillNo ,FMaterialId.FNumber,FMaterialName, F_UCHN_BaseProperty_qtr");
|
||||
|
||||
JsonArray filterString = new JsonArray();
|
||||
JsonObject filterObject = new JsonObject();
|
||||
@ -1513,9 +1522,18 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
filterObject.addProperty("Value", rooteProdet);
|
||||
filterObject.addProperty("Left", "");
|
||||
filterObject.addProperty("Right", "");
|
||||
filterObject.addProperty("Logic", 0);
|
||||
filterObject.addProperty("Logic", 1);
|
||||
filterString.add(filterObject);
|
||||
|
||||
JsonObject filterObject1 = new JsonObject();
|
||||
filterObject1.addProperty("FieldName", "F_HBYT_SCLH"); // 使用传入的 fieldName
|
||||
filterObject1.addProperty("Compare", "67");
|
||||
filterObject1.addProperty("Value", rooteProdet);
|
||||
filterObject1.addProperty("Left", "");
|
||||
filterObject1.addProperty("Right", "");
|
||||
filterObject1.addProperty("Logic", 1);
|
||||
filterString.add(filterObject1);
|
||||
|
||||
json.add("FilterString", filterString);
|
||||
json.addProperty("OrderString", "");
|
||||
json.addProperty("TopRowCount", 0);
|
||||
|
||||
@ -17,9 +17,12 @@ import com.ruoyi.system.domain.bo.ProcessRouteBo;
|
||||
import com.ruoyi.system.domain.dto.excuteDrawing.DataInfo;
|
||||
import com.ruoyi.system.domain.dto.excuteDrawing.ProductInfo;
|
||||
import com.ruoyi.system.domain.dto.excuteDrawing.PwProductionBill;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO;
|
||||
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO;
|
||||
import com.ruoyi.system.domain.vo.ProcessOrderProVo;
|
||||
import com.ruoyi.system.mapper.FigureSaveMapper;
|
||||
import com.ruoyi.system.service.IPcRigidChainService;
|
||||
import com.ruoyi.system.utils.ProductionOrderNoUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -35,6 +38,7 @@ import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.Array;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 生产订单Service业务层处理
|
||||
@ -87,9 +91,12 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
lqw.eq(bo.getQuantity() != null, ProductionOrder::getQuantity, bo.getQuantity());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getMaterial()), ProductionOrder::getMaterial, bo.getMaterial());
|
||||
lqw.eq(bo.getSingleWeight() != null, ProductionOrder::getSingleWeight, bo.getSingleWeight());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getMainName()), ProductionOrder::getMainName, bo.getMainName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getMainPart()), ProductionOrder::getMainPart, bo.getMainPart());
|
||||
lqw.eq(bo.getTotalWeight() != null, ProductionOrder::getTotalWeight, bo.getTotalWeight());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getParentPart()), ProductionOrder::getParentPart, bo.getParentPart());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getParentDrawingNo()), ProductionOrder::getParentDrawingNo, bo.getParentDrawingNo());
|
||||
|
||||
lqw.orderByDesc(ProductionOrder::getParentPart);
|
||||
return lqw;
|
||||
}
|
||||
@ -188,12 +195,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
}
|
||||
PwProductionBill pwProductionBill = new PwProductionBill();
|
||||
String code = orderPro.getProductionOrderNo();
|
||||
String yearPart;
|
||||
if ("-".equals(code.substring(2, 3))) {
|
||||
yearPart = code.substring(3, 5);
|
||||
} else {
|
||||
yearPart = code.substring(2, 4);
|
||||
}
|
||||
String yearPart = ProductionOrderNoUtils.extractYearPart(code);
|
||||
String dirPath = Paths.get("F:", "20" + yearPart).toString();
|
||||
log.info("生产令号:{} 生成路径:{}", orderPro.getProductionOrderNo(), dirPath);
|
||||
// pwProductionBill.setDestpath(dirPath.replace(File.separator, "\\"));
|
||||
@ -342,5 +344,106 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> selectProductionOrderNoOptions(String keyword, Integer limit) {
|
||||
int safeLimit = limit == null ? 200 : limit;
|
||||
if (safeLimit <= 0) {
|
||||
safeLimit = 200;
|
||||
}
|
||||
if (safeLimit > 1000) {
|
||||
safeLimit = 1000;
|
||||
}
|
||||
String kw = StringUtils.isBlank(keyword) ? null : keyword.trim();
|
||||
return baseMapper.selectDistinctProductionOrderNos(kw, safeLimit);
|
||||
}
|
||||
|
||||
private Map<String, Double> buildSingleWeightLookupMap(List<String> drawingNos) {
|
||||
if (drawingNos == null || drawingNos.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
final int batchSize = 500;
|
||||
Map<String, Double> weightMap = new HashMap<>();
|
||||
for (int i = 0; i < drawingNos.size(); i += batchSize) {
|
||||
int end = Math.min(i + batchSize, drawingNos.size());
|
||||
List<String> batch = drawingNos.subList(i, end);
|
||||
List<ProductionOrder> dbRows = baseMapper.selectWeightByDrawingNos(batch);
|
||||
for (ProductionOrder dbRow : dbRows) {
|
||||
if (StringUtils.isNotBlank(dbRow.getDrawingNo()) && dbRow.getSingleWeight() != null) {
|
||||
weightMap.put(dbRow.getDrawingNo().trim(), dbRow.getSingleWeight().doubleValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
List<String> missingCodes = drawingNos.stream()
|
||||
.filter(code -> !weightMap.containsKey(code))
|
||||
.collect(Collectors.toList());
|
||||
for (int i = 0; i < missingCodes.size(); i += batchSize) {
|
||||
int end = Math.min(i + batchSize, missingCodes.size());
|
||||
List<String> batch = missingCodes.subList(i, end);
|
||||
List<ProductionOrder> routeRows = baseMapper.selectDiscWeightByMaterialCodes(batch);
|
||||
for (ProductionOrder routeRow : routeRows) {
|
||||
if (StringUtils.isNotBlank(routeRow.getDrawingNo()) && routeRow.getSingleWeight() != null) {
|
||||
weightMap.put(routeRow.getDrawingNo().trim(), routeRow.getSingleWeight().doubleValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return weightMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Double> resolveSingleWeightByMaterialCodes(Collection<String> materialCodes) {
|
||||
if (materialCodes == null || materialCodes.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<String> drawingNos = materialCodes.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (drawingNos.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return buildSingleWeightLookupMap(drawingNos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProductionOrderWeightExportDTO> matchSingleWeightByMaterialCode(List<ProductionOrderWeightImportDTO> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> drawingNos = rows.stream()
|
||||
.map(ProductionOrderWeightImportDTO::getMaterialCode)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (drawingNos.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Map<String, Double> weightMap = buildSingleWeightLookupMap(drawingNos);
|
||||
List<ProductionOrderWeightExportDTO> result = new ArrayList<>();
|
||||
for (ProductionOrderWeightImportDTO row : rows) {
|
||||
if (row == null || StringUtils.isBlank(row.getMaterialCode())) {
|
||||
continue;
|
||||
}
|
||||
String code = row.getMaterialCode().trim();
|
||||
Double dbWeight = weightMap.get(code);
|
||||
ProductionOrderWeightExportDTO dto = new ProductionOrderWeightExportDTO();
|
||||
dto.setMaterialCode(code);
|
||||
dto.setMaterialName(row.getMaterialName());
|
||||
dto.setSingleWeight(dbWeight);
|
||||
result.add(dto);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double resolveSingleWeightByMaterialCode(String materialCode) {
|
||||
if (StringUtils.isBlank(materialCode)) {
|
||||
return null;
|
||||
}
|
||||
String code = materialCode.trim();
|
||||
return buildSingleWeightLookupMap(Collections.singletonList(code)).get(code);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.dingding.relay.DingMessageRelayService;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@ -30,18 +31,19 @@ import com.ruoyi.system.runner.updatePcessPlanConver;
|
||||
import com.kingdee.bos.webapi.entity.RepoRet;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import java.util.Date;
|
||||
import com.ruoyi.system.service.ISfcOperationPlanningMainService;
|
||||
import com.ruoyi.system.service.support.OperationPlanningResultAssembler;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
@ -57,9 +59,10 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
|
||||
private final SfcOperationPlanningMainMapper baseMapper;
|
||||
private final SfcOperationPlanningDetailMapper detailMapper;
|
||||
private final DingMessageRelayService dingMessageRelayService;
|
||||
|
||||
/**
|
||||
* 同步K3工序计划数据
|
||||
* 同步K3工序计划数据(全量新增与更新)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@ -81,7 +84,12 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
mainEntity.setFid(resultDTO.getFid());
|
||||
mainEntity.setBillNo(resultDTO.getBillNo());
|
||||
mainEntity.setMoNumber(resultDTO.getMoNumber());
|
||||
if (StringUtils.isNotBlank(resultDTO.getBatchNo())) {
|
||||
mainEntity.setHbytSclh(resultDTO.getBatchNo());
|
||||
} else if (mainEntity.getId() == null && StringUtils.isNotBlank(resultDTO.getProjectNo())) {
|
||||
mainEntity.setHbytSclh(resultDTO.getProjectNo());
|
||||
}
|
||||
mainEntity.setFHbytXmh(resultDTO.getProjectNo());
|
||||
mainEntity.setProductCode(resultDTO.getProductCode());
|
||||
mainEntity.setProductName(resultDTO.getProductName());
|
||||
mainEntity.setProSpecification(resultDTO.getProSpecification());
|
||||
@ -104,19 +112,8 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
List<ProcessDetailDTO> processList = resultDTO.getProcessList();
|
||||
if (processList != null && !processList.isEmpty()) {
|
||||
for (ProcessDetailDTO detailDTO : processList) {
|
||||
// 查询是否已经存在该明细记录 (根据 fid 和 entity_entry_id 和 oper_number)
|
||||
SfcOperationPlanningDetail detailEntity;
|
||||
if (detailDTO.getK3DetailId() != null) {
|
||||
detailEntity = detailMapper.selectOne(Wrappers.<SfcOperationPlanningDetail>lambdaQuery()
|
||||
.eq(SfcOperationPlanningDetail::getFid, resultDTO.getFid())
|
||||
.eq(SfcOperationPlanningDetail::getK3DetailId, detailDTO.getK3DetailId()));
|
||||
} else {
|
||||
detailEntity = detailMapper.selectOne(Wrappers.<SfcOperationPlanningDetail>lambdaQuery()
|
||||
.eq(SfcOperationPlanningDetail::getFid, resultDTO.getFid())
|
||||
.eq(SfcOperationPlanningDetail::getEntityEntryId, detailDTO.getEntityEntryId())
|
||||
.eq(detailDTO.getOperNo() != null, SfcOperationPlanningDetail::getOperNumber, detailDTO.getOperNo()));
|
||||
}
|
||||
|
||||
// 先按 k3_detail_id 再按 entity_entry_id(+工序号)兜底,避免金蝶字段空/有切换时漏查导致重复 insert 触发唯一约束
|
||||
SfcOperationPlanningDetail detailEntity = findDetailForSync(resultDTO.getFid(), detailDTO);
|
||||
if (detailEntity == null) {
|
||||
detailEntity = new SfcOperationPlanningDetail();
|
||||
}
|
||||
@ -159,7 +156,6 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
detailEntity.setActivityName(detailDTO.getActivity1Name());
|
||||
detailEntity.setOperDescription(detailDTO.getOperDescription());
|
||||
detailEntity.setEquipment(detailDTO.getEquipment());
|
||||
detailEntity.setK3DetailId(detailDTO.getK3DetailId());
|
||||
|
||||
if (detailEntity.getId() == null) {
|
||||
detailMapper.insert(detailEntity);
|
||||
@ -172,6 +168,51 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步时定位已有明细行:优先 k3_detail_id,未命中再用 entity_entry_id(及工序号)匹配,避免互斥分支漏查。
|
||||
*/
|
||||
private SfcOperationPlanningDetail findDetailForSync(Long fid, ProcessDetailDTO detailDTO) {
|
||||
if (fid == null) {
|
||||
return null;
|
||||
}
|
||||
if (detailDTO.getK3DetailId() != null) {
|
||||
List<SfcOperationPlanningDetail> byK3 = detailMapper.selectList(Wrappers.<SfcOperationPlanningDetail>lambdaQuery()
|
||||
.eq(SfcOperationPlanningDetail::getFid, fid)
|
||||
.eq(SfcOperationPlanningDetail::getK3DetailId, detailDTO.getK3DetailId())
|
||||
.last("LIMIT 2"));
|
||||
if (!byK3.isEmpty()) {
|
||||
if (byK3.size() > 1) {
|
||||
log.warn("工序计划明细 k3_detail_id 重复 fid={} k3DetailId={} 使用首条", fid, detailDTO.getK3DetailId());
|
||||
}
|
||||
return byK3.get(0);
|
||||
}
|
||||
}
|
||||
if (detailDTO.getEntityEntryId() != null) {
|
||||
LambdaQueryWrapper<SfcOperationPlanningDetail> w = Wrappers.<SfcOperationPlanningDetail>lambdaQuery()
|
||||
.eq(SfcOperationPlanningDetail::getFid, fid)
|
||||
.eq(SfcOperationPlanningDetail::getEntityEntryId, detailDTO.getEntityEntryId());
|
||||
if (detailDTO.getOperNo() != null) {
|
||||
w.eq(SfcOperationPlanningDetail::getOperNumber, detailDTO.getOperNo().longValue());
|
||||
}
|
||||
List<SfcOperationPlanningDetail> list = detailMapper.selectList(w);
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (list.size() > 1) {
|
||||
if (detailDTO.getK3DetailId() != null) {
|
||||
for (SfcOperationPlanningDetail row : list) {
|
||||
if (detailDTO.getK3DetailId().equals(row.getK3DetailId())) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("工序计划明细命中多条 fid={} entityEntryId={} operNo={} 使用首条", fid, detailDTO.getEntityEntryId(), detailDTO.getOperNo());
|
||||
}
|
||||
return list.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateOperators(OperationPlanningUpdateOperatorsReq req) {
|
||||
@ -290,9 +331,95 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
detail.setEquipment(item.getEquipment()); // 同步更新设备字段
|
||||
detailMapper.updateById(detail);
|
||||
}
|
||||
sendDispatchNotice(main, details, updateMap);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sendDispatchNotice(SfcOperationPlanningMain main,
|
||||
List<SfcOperationPlanningDetail> details,
|
||||
Map<Long, OperationPlanningUpdateOperatorItem> updateMap) {
|
||||
List<String> receivers = resolveDispatchReceivers(updateMap);
|
||||
if (receivers.isEmpty()) {
|
||||
log.warn("派工通知未发送:本次派工未提供有效人员名字");
|
||||
return;
|
||||
}
|
||||
|
||||
String title = "工序派工通知";
|
||||
String markdown = buildDispatchMarkdown(main, details, updateMap);
|
||||
try {
|
||||
dingMessageRelayService.sendMarkdownToUser(title, markdown, receivers);
|
||||
} catch (Exception e) {
|
||||
// 通知发送失败不影响派工更新主流程
|
||||
log.error("派工通知发送失败, fid={}", main == null ? null : main.getFid(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> resolveDispatchReceivers(Map<Long, OperationPlanningUpdateOperatorItem> updateMap) {
|
||||
if (updateMap == null || updateMap.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Set<String> users = new LinkedHashSet<>();
|
||||
for (OperationPlanningUpdateOperatorItem item : updateMap.values()) {
|
||||
if (item == null || StringUtils.isBlank(item.getOperator())) {
|
||||
continue;
|
||||
}
|
||||
String normalized = item.getOperator().replace(",", ",").trim();
|
||||
if (StringUtils.isBlank(normalized)) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = normalized.split("、");
|
||||
for (String part : parts) {
|
||||
String user = part == null ? null : part.trim();
|
||||
if (StringUtils.isNotBlank(user)) {
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(users);
|
||||
}
|
||||
|
||||
private String buildDispatchMarkdown(SfcOperationPlanningMain main,
|
||||
List<SfcOperationPlanningDetail> details,
|
||||
Map<Long, OperationPlanningUpdateOperatorItem> updateMap) {
|
||||
StringBuilder sb = new StringBuilder(512);
|
||||
sb.append("### 工序派工通知\n\n")
|
||||
.append("- 派工时间: ").append(DateUtil.formatDateTime(new Date())).append("\n")
|
||||
.append("- 生产执行单号: ").append(main.getHbytSclh() == null ? "-" : main.getHbytSclh()).append("\n")
|
||||
.append("- 项目号: ").append(main.getFHbytXmh() == null ? "-" : main.getFHbytXmh()).append("\n")
|
||||
.append("- 产品编码: ").append(main.getProductCode() == null ? "-" : main.getProductCode()).append("\n")
|
||||
.append("- 产品名称: ").append(main.getProductName() == null ? "-" : main.getProductName()).append("\n")
|
||||
.append("---\n");
|
||||
|
||||
for (SfcOperationPlanningDetail detail : details) {
|
||||
OperationPlanningUpdateOperatorItem item = updateMap.get(detail.getK3DetailId());
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String operator = item.getOperator();
|
||||
if (operator != null) {
|
||||
operator = operator.trim();
|
||||
}
|
||||
sb.append("**工序: ").append(detail.getProcessName() == null ? "-" : detail.getProcessName())
|
||||
.append("**\n")
|
||||
.append("- 工序号: ").append(detail.getOperNumber() == null ? "-" : detail.getOperNumber()).append("\n")
|
||||
.append("- 工序数量: ").append(detail.getOperQty() == null ? "-" : detail.getOperQty()).append("\n")
|
||||
.append("- 转入数量: ").append(detail.getTransInQty() == null ? "-" : detail.getTransInQty()).append("\n")
|
||||
.append("- 车间/工作中心: ")
|
||||
.append(detail.getWorkCenterName() == null ? "-" : detail.getWorkCenterName()).append("\n")
|
||||
.append("- 作业: ").append(detail.getProcessName() == null ? "-" : detail.getProcessName()).append("\n")
|
||||
.append("- 作业人员: ").append(StringUtils.isBlank(operator) ? "-" : operator).append("\n")
|
||||
.append("- 工序控制码: ").append(StringUtils.isBlank(detail.getOptCtrlCode()) ? "-" : detail.getOptCtrlCode()).append("\n")
|
||||
.append("- 工序说明: ").append(StringUtils.isBlank(detail.getOperDescription()) ? "-" : detail.getOperDescription()).append("\n")
|
||||
.append("- 设备/厂家: ").append(StringUtils.isBlank(item.getEquipment()) ? "-" : item.getEquipment().trim()).append("\n")
|
||||
.append("- 工序计划开始时间: ")
|
||||
.append(detail.getSeqPlanStartTime() == null ? "-" : DateUtil.formatDateTime(detail.getSeqPlanStartTime())).append("\n")
|
||||
.append("- 工序计划完成时间: ")
|
||||
.append(detail.getSeqPlanFinishTime() == null ? "-" : DateUtil.formatDateTime(detail.getSeqPlanFinishTime())).append("\n")
|
||||
.append("---\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工序计划主
|
||||
*/
|
||||
@ -412,137 +539,7 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
.collect(Collectors.groupingBy(SfcOperationPlanningDetail::getMainId));
|
||||
|
||||
// 6. 组装返回给前端的层级结构
|
||||
List<OperationPlanResultDTO> resultList = new ArrayList<>();
|
||||
for (SfcOperationPlanningMain main : mainList) {
|
||||
OperationPlanResultDTO resultDTO = new OperationPlanResultDTO();
|
||||
resultDTO.setMoNumber(main.getMoNumber());
|
||||
resultDTO.setBatchNo(main.getHbytSclh());
|
||||
resultDTO.setProductCode(main.getProductCode());
|
||||
resultDTO.setProductName(main.getProductName());
|
||||
resultDTO.setBillNo(main.getBillNo());
|
||||
resultDTO.setProSpecification(main.getProSpecification());
|
||||
resultDTO.setPlanStartTime(main.getPlanStartTime() != null ? DateUtil.formatDateTime(main.getPlanStartTime()) : null);
|
||||
resultDTO.setPlanFinishTime(main.getPlanFinishTime() != null ? DateUtil.formatDateTime(main.getPlanFinishTime()) : null);
|
||||
resultDTO.setFid(main.getFid());
|
||||
|
||||
// 获取原生Date以便于后续排序使用
|
||||
resultDTO.setRawPlanStartTime(main.getPlanStartTime());
|
||||
|
||||
// 组装关联的子表数据
|
||||
List<ProcessDetailDTO> processList = new ArrayList<>();
|
||||
List<SfcOperationPlanningDetail> relatedDetails = detailMap.get(main.getId());
|
||||
|
||||
double totalPlanQty = 0.0;
|
||||
double totalOkQty = 0.0;
|
||||
int finishedCount = 0;
|
||||
String mainWorkCenter = "";
|
||||
|
||||
if (relatedDetails != null) {
|
||||
// 计算该单据所有工段的总进度数据
|
||||
mainWorkCenter = relatedDetails.get(0).getWorkCenterName();
|
||||
for (SfcOperationPlanningDetail detail : relatedDetails) {
|
||||
if (detail.getOperQty() != null) totalPlanQty += detail.getOperQty().doubleValue();
|
||||
if (detail.getQualifiedQty() != null) totalOkQty += detail.getQualifiedQty().doubleValue();
|
||||
if ("4".equals(detail.getOperStatus())) {
|
||||
finishedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (SfcOperationPlanningDetail detail : relatedDetails) {
|
||||
ProcessDetailDTO detailDTO = new ProcessDetailDTO();
|
||||
detailDTO.setOperNo(detail.getOperNumber() != null ? detail.getOperNumber().intValue() : null);
|
||||
detailDTO.setProcessName(detail.getProcessName());
|
||||
detailDTO.setWorkCenter(detail.getWorkCenterName());
|
||||
detailDTO.setPlanQty(detail.getOperQty() != null ? detail.getOperQty().doubleValue() : null);
|
||||
detailDTO.setInQty(detail.getTransInQty() != null ? detail.getTransInQty().doubleValue() : null);
|
||||
detailDTO.setOkQty(detail.getQualifiedQty() != null ? detail.getQualifiedQty().doubleValue() : null);
|
||||
detailDTO.setNgQty(detail.getUnqualifiedQty() != null ? detail.getUnqualifiedQty().doubleValue() : null);
|
||||
detailDTO.setTransOutQty(detail.getTransOutQty() != null ? detail.getTransOutQty().doubleValue() : null);
|
||||
detailDTO.setWastageQty(detail.getWastageQty() != null ? detail.getWastageQty().doubleValue() : null);
|
||||
detailDTO.setDepartment(detail.getDepartmentName());
|
||||
detailDTO.setOperator(detail.getOperator());
|
||||
if (detailDTO.getPlanQty() != null && detailDTO.getInQty() != null && detailDTO.getPlanQty() > 0) {
|
||||
BigDecimal progress = BigDecimal.valueOf(detailDTO.getInQty())
|
||||
.multiply(BigDecimal.valueOf(100))
|
||||
.divide(BigDecimal.valueOf(detailDTO.getPlanQty()), 1, RoundingMode.HALF_UP);
|
||||
if (progress.compareTo(BigDecimal.ZERO) < 0) {
|
||||
progress = BigDecimal.ZERO;
|
||||
} else if (progress.compareTo(BigDecimal.valueOf(100)) > 0) {
|
||||
progress = BigDecimal.valueOf(100);
|
||||
}
|
||||
detailDTO.setProgress(progress.doubleValue());
|
||||
}
|
||||
|
||||
// 状态映射
|
||||
String statusStr = detail.getOperStatus();
|
||||
if ("1".equals(statusStr)) detailDTO.setK3Status("计划");
|
||||
else if ("2".equals(statusStr)) detailDTO.setK3Status("计划下达");
|
||||
else if ("3".equals(statusStr)) detailDTO.setK3Status("开工");
|
||||
else if ("4".equals(statusStr)) detailDTO.setK3Status("已完工");
|
||||
else if ("5".equals(statusStr)) detailDTO.setK3Status("结案");
|
||||
else if ("6".equals(statusStr)) detailDTO.setK3Status("结算");
|
||||
else detailDTO.setK3Status(statusStr);
|
||||
|
||||
double inQty = detailDTO.getInQty() == null ? 0D : detailDTO.getInQty();
|
||||
double okQty = detailDTO.getOkQty() == null ? 0D : detailDTO.getOkQty();
|
||||
double ngQty = detailDTO.getNgQty() == null ? 0D : detailDTO.getNgQty();
|
||||
double transOutQty = detailDTO.getTransOutQty() == null ? 0D : detailDTO.getTransOutQty();
|
||||
if ("5".equals(statusStr) || "6".equals(statusStr)) {
|
||||
detailDTO.setStatus("已关闭");
|
||||
} else if (inQty == 0D) {
|
||||
detailDTO.setStatus("未开工");
|
||||
} else if (transOutQty > 0D) {
|
||||
detailDTO.setStatus("已转出");
|
||||
} else if (okQty + ngQty >= inQty) {
|
||||
detailDTO.setStatus("工序完成");
|
||||
} else if (okQty + ngQty > 0D) {
|
||||
detailDTO.setStatus("质检中");
|
||||
} else {
|
||||
detailDTO.setStatus("生产中");
|
||||
}
|
||||
|
||||
detailDTO.setSeqPlanStartTime(detail.getSeqPlanStartTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanStartTime()) : null);
|
||||
detailDTO.setSeqPlanFinishTime(detail.getSeqPlanFinishTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanFinishTime()) : null);
|
||||
detailDTO.setOperPlanStartTime(detail.getOperPlanStartTime() != null ? DateUtil.formatDateTime(detail.getOperPlanStartTime()) : null);
|
||||
detailDTO.setOperPlanFinishTime(detail.getOperPlanFinishTime() != null ? DateUtil.formatDateTime(detail.getOperPlanFinishTime()) : null);
|
||||
detailDTO.setOptCtrlCode(detail.getOptCtrlCode());
|
||||
detailDTO.setKeyOper(detail.getKeyOper());
|
||||
detailDTO.setActivity1Name(detail.getActivityName());
|
||||
detailDTO.setOperDescription(detail.getOperDescription());
|
||||
detailDTO.setEquipment(detail.getEquipment());
|
||||
detailDTO.setK3DetailId(detail.getK3DetailId());
|
||||
detailDTO.setEntityEntryId(detail.getEntityEntryId());
|
||||
|
||||
processList.add(detailDTO);
|
||||
}
|
||||
|
||||
resultDTO.setWorkCenter(mainWorkCenter);
|
||||
resultDTO.setOverallPlanQty(totalPlanQty);
|
||||
resultDTO.setOverallOkQty(totalOkQty);
|
||||
|
||||
double progress = 0.0;
|
||||
if (totalPlanQty > 0) {
|
||||
progress = (totalOkQty / totalPlanQty) * 100;
|
||||
}
|
||||
resultDTO.setOverallProgress(Math.min(100.0, Math.max(0.0, progress)));
|
||||
|
||||
// 计算总体状态
|
||||
if (relatedDetails.isEmpty() || (finishedCount == 0 && totalOkQty == 0)) {
|
||||
resultDTO.setOverallStatus("未开工");
|
||||
} else if (finishedCount == relatedDetails.size()) {
|
||||
resultDTO.setOverallStatus("已完工");
|
||||
} else {
|
||||
resultDTO.setOverallStatus("生产中");
|
||||
}
|
||||
}
|
||||
resultDTO.setProcessList(processList);
|
||||
resultList.add(resultDTO);
|
||||
}
|
||||
|
||||
// 对当前页结果进行排序:先按生产令号升序,再按计划开始时间升序
|
||||
resultList.sort(Comparator
|
||||
.comparing(OperationPlanResultDTO::getBatchNo, Comparator.nullsLast(String::compareTo))
|
||||
.thenComparing(OperationPlanResultDTO::getRawPlanStartTime, Comparator.nullsLast(Date::compareTo)));
|
||||
List<OperationPlanResultDTO> resultList = OperationPlanningResultAssembler.assemble(mainList, detailMap);
|
||||
|
||||
// 构造返回带 total 的 TableDataInfo
|
||||
TableDataInfo<OperationPlanResultDTO> rspData = new TableDataInfo<>();
|
||||
@ -559,7 +556,13 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
lqw.eq(bo.getFid() != null, SfcOperationPlanningMain::getFid, bo.getFid());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getBillNo()), SfcOperationPlanningMain::getBillNo, bo.getBillNo());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getMoNumber()), SfcOperationPlanningMain::getMoNumber, bo.getMoNumber());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getHbytSclh()), SfcOperationPlanningMain::getHbytSclh, bo.getHbytSclh());
|
||||
if (StringUtils.isNotBlank(bo.getHbytSclh())) {
|
||||
String keyword = bo.getHbytSclh().trim();
|
||||
lqw.and(w -> w.eq(SfcOperationPlanningMain::getHbytSclh, keyword)
|
||||
.or()
|
||||
.like(SfcOperationPlanningMain::getFHbytXmh, keyword));
|
||||
}
|
||||
lqw.like(StringUtils.isNotBlank(bo.getFHbytXmh()), SfcOperationPlanningMain::getFHbytXmh, bo.getFHbytXmh());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getProductCode()), SfcOperationPlanningMain::getProductCode, bo.getProductCode());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getProductName()), SfcOperationPlanningMain::getProductName, bo.getProductName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getProSpecification()), SfcOperationPlanningMain::getProSpecification, bo.getProSpecification());
|
||||
|
||||
BIN
ruoyi-system/src/main/resources/font/arial unicode ms.ttf
Normal file
BIN
ruoyi-system/src/main/resources/font/arial unicode ms.ttf
Normal file
Binary file not shown.
@ -21,6 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="unit" column="unit"/>
|
||||
<result property="quantity" column="quantity"/>
|
||||
<result property="isEnterpriseStandard" column="is_enterprise_standard"/>
|
||||
<result property="isDownload" column="is_download"/>
|
||||
<result property="drawingPath" column="drawing_path"/>
|
||||
<result property="bomStatus" column="bom_status"/>
|
||||
<result property="routeStatus" column="route_status"/>
|
||||
@ -35,6 +36,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="drawingTime" column="drawing_time"/>
|
||||
<result property="auditCardId" column="audit_card_id"/>
|
||||
<result property="notificationCardId" column="notification_card_id"/>
|
||||
<result property="routePlanSendCount" column="route_plan_send_count"/>
|
||||
</resultMap>
|
||||
<!-- 根据项目编号查询 -->
|
||||
<select id="selectByProjectNumber" resultType="com.ruoyi.system.domain.ProcessOrderPro">
|
||||
|
||||
@ -20,8 +20,41 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="mainName" column="main_name"/>
|
||||
<result property="mainPart" column="main_part"/>
|
||||
<result property="batchQuantity" column="batch_quantity"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectWeightByDrawingNos" resultType="com.ruoyi.system.domain.ProductionOrder">
|
||||
select p.drawing_no as drawingNo,
|
||||
p.single_weight as singleWeight
|
||||
from production_order p
|
||||
inner join (
|
||||
select drawing_no, max(id) as max_id
|
||||
from production_order
|
||||
where drawing_no in
|
||||
<foreach collection="drawingNos" item="drawingNo" open="(" separator="," close=")">
|
||||
#{drawingNo}
|
||||
</foreach>
|
||||
group by drawing_no
|
||||
) t on p.id = t.max_id
|
||||
where p.single_weight is not null
|
||||
</select>
|
||||
|
||||
<select id="selectDiscWeightByMaterialCodes" resultType="com.ruoyi.system.domain.ProductionOrder">
|
||||
select p.material_code as drawingNo,
|
||||
p.disc_weight as singleWeight
|
||||
from process_route p
|
||||
inner join (
|
||||
select material_code, max(id) as max_id
|
||||
from process_route
|
||||
where material_code in
|
||||
<foreach collection="materialCodes" item="materialCode" open="(" separator="," close=")">
|
||||
#{materialCode}
|
||||
</foreach>
|
||||
group by material_code
|
||||
) t on p.id = t.max_id
|
||||
where p.disc_weight is not null
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="billNo" column="bill_no"/>
|
||||
<result property="moNumber" column="mo_number"/>
|
||||
<result property="hbytSclh" column="hbyt_sclh"/>
|
||||
<result property="fHbytXmh" column="hbyt_xmh"/>
|
||||
<result property="productCode" column="product_code"/>
|
||||
<result property="productName" column="product_name"/>
|
||||
<result property="proSpecification" column="pro_specification"/>
|
||||
|
||||
@ -94,7 +94,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
AND d.work_center_name = #{workCenterName}
|
||||
</if>
|
||||
GROUP BY m.mo_number
|
||||
ORDER BY m.plan_start_time DESC, progress DESC
|
||||
ORDER BY MAX(m.plan_start_time) DESC, progress DESC
|
||||
LIMIT #{topN}
|
||||
</select>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user