From c86cc27510394a5981b4ac1cb8473db70a792081 Mon Sep 17 00:00:00 2001 From: mgw <1392924357@qq.com> Date: Wed, 21 Aug 2024 15:38:05 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B7=A5=E5=8D=95?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sf_machine_connect/controllers/controllers.py | 257 +++++++++++++++++- 1 file changed, 255 insertions(+), 2 deletions(-) diff --git a/sf_machine_connect/controllers/controllers.py b/sf_machine_connect/controllers/controllers.py index 4f926a5b..0a770f9d 100644 --- a/sf_machine_connect/controllers/controllers.py +++ b/sf_machine_connect/controllers/controllers.py @@ -2,8 +2,9 @@ import re import ast import json +import base64 import logging -from datetime import datetime +from datetime import datetime, timedelta from odoo import http from odoo.http import request @@ -151,9 +152,10 @@ class Sf_Dashboard_Connect(http.Controller): 'first_online_duration': first_online_duration, # 停机时间:关机时间 - 运行时间 # 停机时长:关机时间 - 初次上线时间 + 'img': base64.b64encode(machine_data.machine_tool_picture).decode('utf-8'), }) - return json.JSONEncoder().encode(res) + return json.dumps(res) except Exception as e: logging.info('前端请求机床数据失败,原因:%s' % e) res['status'] = -1 @@ -282,3 +284,254 @@ class Sf_Dashboard_Connect(http.Controller): res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} logging.info('CNCList error:%s' % e) return json.JSONEncoder().encode(res) + + # 返回产线列表 + + @http.route('/api/LineList', type='http', auth='public', methods=['GET', 'POST'], csrf=False, + + cors="*") + def LineList(self, **kw): + """ + 获取产线列表 + :param kw: + :return: + """ + + try: + res = {'Succeed': True} + line_list_obj = request.env['sf.production.line'].sudo().search([('name', 'ilike', 'CNC')]) + line_list = list(map(lambda x: x.name, line_list_obj)) + print('line_list: %s' % line_list) + res['LineList'] = line_list + + except Exception as e: + res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} + logging.info('LineList error:%s' % e) + + return json.JSONEncoder().encode(res) + + # 获取产线产量相关 + + @http.route('/api/LineProduct', type='http', auth='public', methods=['GET', 'POST'], csrf=False, + + cors="*") + def LineProduct(self, **kw): + """ + 获取产线产量相关 + :param kw: + :return: + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求产线产量数据的参数为:%s' % kw) + + try: + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + print('line_list: %s' % line_list) + for line in line_list: + plan_data = plan_obj.search([('production_line_id.name', '=', line)]) + # 工单总量 + plan_data_total_counts = plan_obj.search_count([('production_line_id.name', '=', line)]) + # 工单完成量 + plan_data_finish_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'not in', ['draft'])]) + # 工单计划量 + plan_data_plan_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'not in', ['finished'])]) + # 工单不良累计 + plan_data_fault_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('production_id.state', 'in', ['scrap', 'cancel'])]) + # 工单完成率 + finishe_rate = round( + (plan_data_finish_counts / plan_data_total_counts if plan_data_total_counts > 0 else 0), 3) + + # 工单进度偏差 + plan_data_progress_deviation = plan_data_finish_counts - plan_data_plan_counts + + if plan_data: + data = { + 'plan_data_total_counts': plan_data_total_counts, + 'plan_data_finish_counts': plan_data_finish_counts, + 'plan_data_plan_counts': plan_data_plan_counts, + 'plan_data_fault_counts': plan_data_fault_counts, + 'finishe_rate': finishe_rate, + 'plan_data_progress_deviation': plan_data_progress_deviation, + } + res['data'][line] = data + + return json.dumps(res) # 注意使用 json.dumps 而不是直接用 json.JSONEncoder().encode() + + except Exception as e: + logging.info('前端请求产线产量数据失败,原因:%s' % e) + res['status'] = -1 + res['message'] = '前端请求产线产量数据失败,原因:%s' % e + return json.dumps(res) + + # 日完成量统计 + class DailyFinishCount(http.Controller): + @http.route('/api/DailyFinishCount', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def DailyFinishCount(self, **kw): + """ + 获取日完成量统计 + :param kw: + :return: + """ + res = {'status': 1, 'message': '成功', 'data': []} + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + print('line_list: %s' % line_list) + + def get_date_list(start_date, end_date): + date_list = [] + current_date = start_date + while current_date <= end_date: + date_list.append(current_date) + current_date += timedelta(days=1) + return date_list + + for line in line_list: + date_list = get_date_list(begin_time, end_time) + order_counts = [] + + date_field_name = 'actual_end_time' # 替换为你模型中的实际字段名 + + for date in date_list: + next_day = date + timedelta(days=1) + orders = plan_obj.search([('production_line_id.name', '=', line), ('state', 'not in', ['draft']), + (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), + (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) + ]) + order_counts.append({ + 'date': date.strftime('%Y-%m-%d'), + 'order_count': len(orders) + }) + + res['data'] = order_counts + return json.dumps(res) + + # # 实时产量 + # @http.route('/api/RealTimeProduct', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + # def RealTimeProduct(self, **kw): + # """ + # 获取实时产量 + # :param kw: + # :return: + # """ + # + # def get_real_time_product(line_list): + # res = {'status': 1, 'message': '成功', 'data': []} + # plan_obj = request.env['sf.production.plan'].sudo() + # for line in line_list: + # plan_data = plan_obj.search([('production_line_id.name', '=', line)]) + + # 工单明细 + @http.route('/api/OrderDetail', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def OrderDetail(self, **kw): + """ + 获取工单明细 + :param kw: + :return: + """ + + res = {'status': 1, 'message': '成功', 'not_done_data': [], 'done_data': []} + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + print('line_list: %s' % line_list) + + for line in line_list: + # 未完成订单 + not_done_orders = plan_obj.search( + [('production_line_id.name', '=', line), ('state', 'not in', ['finished'])]) + print(not_done_orders) + # 完成订单 + finish_orders = plan_obj.search([('production_line_id.name', '=', line), ('state', 'in', ['finished'])]) + print(finish_orders) + + # 获取所有未完成订单的ID列表 + order_ids = [order.id for order in not_done_orders] + # 获取所有已完成订单的ID列表 + finish_order_ids = [order.id for order in finish_orders] + + # 对ID进行排序 + sorted_order_ids = sorted(order_ids) + + finish_sorted_order_ids = sorted(finish_order_ids) + + # 创建ID与序号的对应关系 + id_to_sequence = {order_id: index + 1 for index, order_id in enumerate(sorted_order_ids)} + + finish_id_to_sequence = {order_id: index + 1 for index, order_id in enumerate(finish_sorted_order_ids)} + + # # 输出结果或进一步处理 + # for order_id, sequence in id_to_sequence.items(): + # print(f"Order ID: {order_id} - Sequence: {sequence}") + + for order in not_done_orders: + blank_name = '' + try: + blank_name = order.production_id.move_raw_ids[0].product_id.name + except: + continue + # blank_name = 'R-S00109-1 [碳素结构钢 Q235-118.0 * 72.0 * 21.0]' + # 正则表达式 + material_pattern = r'\[(.*?)-' # 从 [ 开始,碰到 - 停止 + dimensions = blank_name.split('-')[-1].split(']')[0] + + # 匹配材料名称 + material_match = re.search(material_pattern, blank_name) + material = material_match.group(1) if material_match else 'No match found' + + state_dict = { + 'draft': '待排程', + 'done': '已排程', + 'processing': '生产中', + 'finished': '已完成' + } + + line_dict = { + 'sequence': id_to_sequence[order.id], + 'workorder_name': order.name, + 'blank_name': blank_name, + 'material': material, + 'dimensions': dimensions, + 'order_qty': order.product_qty, + 'state': state_dict[order.state], + + } + res['not_done_data'].append(line_dict) + + for finish_order in finish_orders: + blank_name = '' + try: + blank_name = finish_order.production_id.move_raw_ids[0].product_id.name + except: + continue + + material_pattern = r'\[(.*?)-' # 从 [ 开始,碰到 - 停止 + dimensions = blank_name.split('-')[-1].split(']')[0] + + # 匹配材料名称 + material_match = re.search(material_pattern, blank_name) + material = material_match.group(1) if material_match else 'No match found' + + line_dict = { + 'sequence': finish_id_to_sequence[finish_order.id], + 'workorder_name': finish_order.name, + 'blank_name': blank_name, + 'material': material, + 'dimensions': dimensions, + 'order_qty': finish_order.product_qty, + 'finish_time': finish_order.actual_end_time.strftime('%Y-%m-%d %H:%M:%S'), + + } + res['done_data'].append(line_dict) + + return json.dumps(res) From b26919a40da50cac3486883b67f7ec691fbf76da Mon Sep 17 00:00:00 2001 From: mgw <1392924357@qq.com> Date: Wed, 21 Aug 2024 17:44:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E5=85=B6=E6=98=93=E4=BA=8E=E5=8F=96=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sf_machine_connect/controllers/controllers.py | 95 +++++++++++++++---- 1 file changed, 75 insertions(+), 20 deletions(-) diff --git a/sf_machine_connect/controllers/controllers.py b/sf_machine_connect/controllers/controllers.py index 0a770f9d..51a5bd92 100644 --- a/sf_machine_connect/controllers/controllers.py +++ b/sf_machine_connect/controllers/controllers.py @@ -376,7 +376,7 @@ class Sf_Dashboard_Connect(http.Controller): :param kw: :return: """ - res = {'status': 1, 'message': '成功', 'data': []} + res = {'status': 1, 'message': '成功', 'data': {}} plan_obj = request.env['sf.production.plan'].sudo() line_list = ast.literal_eval(kw['line_list']) begin_time_str = kw['begin_time'].strip('"') @@ -409,24 +409,73 @@ class Sf_Dashboard_Connect(http.Controller): 'date': date.strftime('%Y-%m-%d'), 'order_count': len(orders) }) + # 外面包一层,没什么是包一层不能解决的,包一层就能区分了,类似于包一层div + # 外面包一层的好处是,可以把多个数据结构打包在一起,方便前端处理 - res['data'] = order_counts - return json.dumps(res) + # date_list_dict = {line: order_counts} - # # 实时产量 - # @http.route('/api/RealTimeProduct', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") - # def RealTimeProduct(self, **kw): - # """ - # 获取实时产量 - # :param kw: - # :return: - # """ - # - # def get_real_time_product(line_list): - # res = {'status': 1, 'message': '成功', 'data': []} - # plan_obj = request.env['sf.production.plan'].sudo() - # for line in line_list: - # plan_data = plan_obj.search([('production_line_id.name', '=', line)]) + res['data'][line] = order_counts + return json.dumps(res) + + # 实时产量 + @http.route('/api/RealTimeProduct', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def RealTimeProduct(self, **kw): + """ + 获取实时产量 + :param kw: + :return: + """ + res = {'status': 1, 'message': '成功', 'data': {}} + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + + def get_hourly_intervals(start_time, end_time): + intervals = [] + current_time = start_time + while current_time < end_time: + next_hour = current_time + timedelta(hours=1) + intervals.append((current_time, min(next_hour, end_time))) + current_time = next_hour + return intervals + + # 当班计划量 + for line in line_list: + plan_order_nums = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'not in', ['draft']), + ('date_planned_start', '>=', begin_time), + ('date_planned_start', '<', end_time) + ]) + finish_order_nums = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'in', ['finished']), + ('date_planned_start', '>=', begin_time), + ('date_planned_start', '<', end_time) + ]) + hourly_intervals = get_hourly_intervals(begin_time, end_time) + production_counts = [] + + for start, end in hourly_intervals: + orders = plan_obj.search([ + ('actual_end_time', '>=', start.strftime('%Y-%m-%d %H:%M:%S')), + ('actual_end_time', '<', end.strftime('%Y-%m-%d %H:%M:%S')), + ('production_line_id.name', '=', line) + ]) + production_counts.append({ + 'start_time': start.strftime('%Y-%m-%d %H:%M:%S'), + 'end_time': end.strftime('%Y-%m-%d %H:%M:%S'), + 'production_count': len(orders) + }) + production_counts_dict = {'production_counts': production_counts, + 'plan_order_nums': plan_order_nums, + 'finish_order_nums': finish_order_nums, + } + + res['data'][line] = production_counts_dict + # res['data'].append({line: production_counts}) + return json.dumps(res) # 工单明细 @http.route('/api/OrderDetail', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") @@ -437,7 +486,8 @@ class Sf_Dashboard_Connect(http.Controller): :return: """ - res = {'status': 1, 'message': '成功', 'not_done_data': [], 'done_data': []} + # res = {'status': 1, 'message': '成功', 'not_done_data': [], 'done_data': []} + res = {'status': 1, 'message': '成功', 'data': {}} plan_obj = request.env['sf.production.plan'].sudo() line_list = ast.literal_eval(kw['line_list']) begin_time_str = kw['begin_time'].strip('"') @@ -445,6 +495,9 @@ class Sf_Dashboard_Connect(http.Controller): begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') print('line_list: %s' % line_list) + not_done_data = [] + done_data = [] + final_data = {} for line in line_list: # 未完成订单 @@ -506,7 +559,7 @@ class Sf_Dashboard_Connect(http.Controller): 'state': state_dict[order.state], } - res['not_done_data'].append(line_dict) + not_done_data.append(line_dict) for finish_order in finish_orders: blank_name = '' @@ -532,6 +585,8 @@ class Sf_Dashboard_Connect(http.Controller): 'finish_time': finish_order.actual_end_time.strftime('%Y-%m-%d %H:%M:%S'), } - res['done_data'].append(line_dict) + done_data.append(line_dict) + # 开始包一层 + res['data'][line] = {'not_done_data': not_done_data, 'done_data': done_data} return json.dumps(res) From 5df3de7dcd0deee52f2b5e80aa1f6c21dff28ca3 Mon Sep 17 00:00:00 2001 From: mgw <1392924357@qq.com> Date: Fri, 23 Aug 2024 10:05:17 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=BF=94=E5=B7=A5?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sf_machine_connect/controllers/controllers.py | 122 +++++++++++++++++- sf_manufacturing/models/mrp_workorder.py | 3 + 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/sf_machine_connect/controllers/controllers.py b/sf_machine_connect/controllers/controllers.py index 51a5bd92..a40b148d 100644 --- a/sf_machine_connect/controllers/controllers.py +++ b/sf_machine_connect/controllers/controllers.py @@ -4,10 +4,20 @@ import ast import json import base64 import logging +import psycopg2 from datetime import datetime, timedelta from odoo import http from odoo.http import request +# 数据库连接配置 +db_config = { + "database": "timeseries_db", + "user": "postgres", + "password": "postgres", + "port": "5432", + "host": "172.16.10.98" +} + def convert_to_seconds(time_str): # 修改正则表达式,使 H、M、S 部分可选 @@ -152,7 +162,7 @@ class Sf_Dashboard_Connect(http.Controller): 'first_online_duration': first_online_duration, # 停机时间:关机时间 - 运行时间 # 停机时长:关机时间 - 初次上线时间 - 'img': base64.b64encode(machine_data.machine_tool_picture).decode('utf-8'), + 'img': f'data:image/png;base64,{machine_data.machine_tool_picture.decode("utf-8")}', }) return json.dumps(res) @@ -341,6 +351,12 @@ class Sf_Dashboard_Connect(http.Controller): # 工单不良累计 plan_data_fault_counts = plan_obj.search_count( [('production_line_id.name', '=', line), ('production_id.state', 'in', ['scrap', 'cancel'])]) + + # 工单返工数量 + + plan_data_rework_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('production_id.state', 'in', ['rework'])]) + # 工单完成率 finishe_rate = round( (plan_data_finish_counts / plan_data_total_counts if plan_data_total_counts > 0 else 0), 3) @@ -356,6 +372,7 @@ class Sf_Dashboard_Connect(http.Controller): 'plan_data_fault_counts': plan_data_fault_counts, 'finishe_rate': finishe_rate, 'plan_data_progress_deviation': plan_data_progress_deviation, + 'plan_data_rework_counts': plan_data_rework_counts } res['data'][line] = data @@ -405,9 +422,22 @@ class Sf_Dashboard_Connect(http.Controller): (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) ]) + + rework_orders = plan_obj.search( + [('production_line_id.name', '=', line), ('state', 'in', ['rework']), + (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), + (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) + ]) + not_passed_orders = plan_obj.search( + [('production_line_id.name', '=', line), ('state', 'in', ['scrap', 'cancel']), + (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), + (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) + ]) order_counts.append({ 'date': date.strftime('%Y-%m-%d'), - 'order_count': len(orders) + 'order_count': len(orders), + 'rework_orders': len(rework_orders), + 'not_passed_orders': len(not_passed_orders) }) # 外面包一层,没什么是包一层不能解决的,包一层就能区分了,类似于包一层div # 外面包一层的好处是,可以把多个数据结构打包在一起,方便前端处理 @@ -590,3 +620,91 @@ class Sf_Dashboard_Connect(http.Controller): # 开始包一层 res['data'][line] = {'not_done_data': not_done_data, 'done_data': done_data} return json.dumps(res) + + # 查询pg库来获得待机次数 + @http.route('/api/IdleAlarmCount', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def idle_count(self, **kw): + """ + 查询设备的待机次数 + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求机床数据的参数为:%s' % kw) + + # 连接数据库 + conn = psycopg2.connect(**db_config) + cur = conn.cursor() + try: + # 获取请求的机床数据 + machine_list = ast.literal_eval(kw['machine_list']) + idle_times = [] + idle_dict = {} + + for item in machine_list: + sql = ''' + SELECT idle_start_time,alarm_time,alarm_repair_time FROM device_data WHERE device_name = %s; + ''' + # 执行SQL命令 + cur.execute(sql, (item,)) + result = cur.fetchall() + # # print('result', result) + # + # # 将查询结果添加到idle_times列表中 + # idle_times = [row[0] for row in result if row[0] is not None] + # + # # 对结果去重 + # unique_idle_times = set(idle_times) + # # print('unique_idle_times', unique_idle_times) + # + # # 统计去重后的数量 + # idle_count = len(unique_idle_times) + # # idle_dict[item] = idle_count + # + # res['data'][item] = idle_count + + total_alarm_time = 0 + alarm_count = 0 + alarm_time_list = [] + idle_times = [] + alarm_times = [] + + for row in result: + idle_start_time = row[0] + alarm_time = row[1] + alarm_repair_time = row[2] + + alarm_time_list.append(alarm_time) # 将时长累加,以秒为单位 + idle_times.append(idle_start_time) + # if alarm_repair_time is not None: + # alarm_times.append(alarm_repair_time) + alarm_times.append(alarm_repair_time) + + # 对结果去重 + unique_total_alarm_time = set(alarm_time_list) + unique_idle_times = set(idle_times) + unique_alarm_times = set(alarm_times) + + # 统计去重后的数量 + idle_count = len(unique_idle_times) + + for alarm_time in unique_total_alarm_time: + if alarm_time is not None: + total_alarm_time += abs(float(alarm_time)) + + alarm_count = len(unique_alarm_times) if unique_alarm_times else 0 + alarm_count = alarm_count if total_alarm_time else 0 + + # 存储待机次数和总待机时长(单位:秒) + res['data'][item] = { + 'idle_count': idle_count, + 'total_alarm_time': total_alarm_time / 3600, # 以秒为单位 + 'alarm_count': alarm_count + } + + # 返回统计结果 + return json.dumps(res) + except Exception as e: + print(f"An error occurred: {e}") + return json.dumps(res) + finally: + cur.close() + conn.close() diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index 6f6a4be9..a69160cb 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -1167,6 +1167,9 @@ class ResMrpWorkOrder(models.Model): record.process_state = '待解除装夹' # record.write({'process_state': '待加工'}) record.production_id.process_state = '待解除装夹' + self.env['sf.production.plan'].sudo().search([('name', '=', record.production_id.name)]).write({ + 'state': 'finished' + }) record.production_id.write({'detection_result_ids': [(0, 0, { 'rework_reason': record.reason, 'detailed_reason': record.detailed_reason,