diff --git a/jikimo_work_reporting_api/__init__.py b/jikimo_work_reporting_api/__init__.py new file mode 100644 index 00000000..9e5827f9 --- /dev/null +++ b/jikimo_work_reporting_api/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models diff --git a/jikimo_work_reporting_api/__manifest__.py b/jikimo_work_reporting_api/__manifest__.py new file mode 100644 index 00000000..502ce9d5 --- /dev/null +++ b/jikimo_work_reporting_api/__manifest__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +{ + 'name': '机企猫 报工系统API', + 'version': '1.0.0', + 'summary': """ 机企猫 报工系统API """, + 'author': '机企猫', + 'website': 'https://xt.sf.jikimo.com', + 'category': 'sf', + 'depends': ['base', 'sf_maintenance', 'jikimo_mini_program'], + 'data': [ + + ], + + 'application': True, + 'installable': True, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/jikimo_work_reporting_api/controllers/__init__.py b/jikimo_work_reporting_api/controllers/__init__.py new file mode 100644 index 00000000..757b12a1 --- /dev/null +++ b/jikimo_work_reporting_api/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/jikimo_work_reporting_api/controllers/main.py b/jikimo_work_reporting_api/controllers/main.py new file mode 100644 index 00000000..76b55dc9 --- /dev/null +++ b/jikimo_work_reporting_api/controllers/main.py @@ -0,0 +1,42 @@ +import json +from odoo import http +from odoo.http import request +from odoo.addons.sf_machine_connect.models.ftp_operate import transfer_nc_files + +class MainController(http.Controller): + + @http.route('/api/manual_download_program', type='json', methods=['POST'], auth='wechat_token', cors='*') + def manual_download_program(self): + """ + 人工线下加工传输编程文件 + """ + data = json.loads(request.httprequest.data) + maintenance_equipment_name = data.get('maintenance_equipment_name') + model_id = data.get('model_id') + if not maintenance_equipment_name or not model_id: + return {'code': 400, 'message': '参数错误'} + maintenance_equipment = request.env['maintenance.equipment'].sudo().search([('name', '=', maintenance_equipment_name)], limit=1) + if not maintenance_equipment: + return {'code': 400, 'message': '机床不存在'} + ftp_resconfig = request.env['res.config.settings'].sudo().get_values() + source_ftp_info = { + 'host': ftp_resconfig['ftp_host'], + 'port': int(ftp_resconfig['ftp_port']), + 'username': ftp_resconfig['ftp_user'], + 'password': ftp_resconfig['ftp_password'] + } + target_ftp_info = { + 'host': maintenance_equipment.ftp_host, + 'port': int(maintenance_equipment.ftp_port), + 'username': maintenance_equipment.ftp_username, + 'password': maintenance_equipment.ftp_password + } + # 传输nc文件 + if transfer_nc_files( + source_ftp_info, + target_ftp_info, + '/' + str(model_id), + '/home/jikimo/testdir'): + return {'code': 200, 'message': 'success'} + else: + return {'code': 500, 'message': '传输失败'} diff --git a/jikimo_work_reporting_api/models/__init__.py b/jikimo_work_reporting_api/models/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/jikimo_work_reporting_api/models/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py b/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py index 0ab0d6ae..f4a49c1f 100644 --- a/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py +++ b/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py @@ -52,10 +52,10 @@ class JikimoWorkorderException(models.Model): def _get_message(self, message_queue_ids): contents, _ = super(JikimoWorkorderException, self)._get_message(message_queue_ids) - url = self.env['ir.config_parameter'].get_param('web.base.url') + base_url = self.env['ir.config_parameter'].get_param('web.base.url') action_id = self.env.ref('mrp.mrp_production_action').id for index, content in enumerate(contents): exception_id = self.env['jikimo.workorder.exception'].browse(message_queue_ids[index].res_id) - url = url + '/web#id=%s&view_type=form&action=%s' % (exception_id.workorder_id.production_id.id, action_id) + url = base_url + '/web#id=%s&view_type=form&action=%s' % (exception_id.workorder_id.production_id.id, action_id) contents[index] = content.replace('{{url}}', url) return contents, message_queue_ids diff --git a/quality_control/models/quality.py b/quality_control/models/quality.py index 47b21dc7..9be466f5 100644 --- a/quality_control/models/quality.py +++ b/quality_control/models/quality.py @@ -206,7 +206,7 @@ class QualityCheck(models.Model): ('NG', 'NG') ], string='出厂检验报告结果', default='OK') measure_operator = fields.Many2one('res.users', string='操机员') - quality_manager = fields.Many2one('res.users', string='质检员', compute='_compute_quality_manager', store=True) + quality_manager = fields.Many2one('res.users', string='质检员', compute='_compute_quality_manager') @api.depends('measure_line_ids') def _compute_quality_manager(self): diff --git a/quality_control/views/quality_views.xml b/quality_control/views/quality_views.xml index 38de9e4e..528003e3 100644 --- a/quality_control/views/quality_views.xml +++ b/quality_control/views/quality_views.xml @@ -267,7 +267,7 @@ - + diff --git a/sf_base/commons/Printer.py b/sf_base/commons/Printer.py index 563d9dea..e9d31282 100644 --- a/sf_base/commons/Printer.py +++ b/sf_base/commons/Printer.py @@ -8,6 +8,7 @@ class Printer(models.Model): name = fields.Char(string='名称', required=True) ip_address = fields.Char(string='IP 地址', required=True) port = fields.Integer(string='端口', default=9100) + type = fields.Selection([('zpl', 'ZPL'), ('normal', '普通')], string='类型', default='zpl') class TableStyle(models.Model): diff --git a/sf_base/commons/common.py b/sf_base/commons/common.py index 9f359c9c..27b56038 100644 --- a/sf_base/commons/common.py +++ b/sf_base/commons/common.py @@ -2,7 +2,16 @@ import time, datetime import hashlib from odoo import models +from typing import Optional import socket +import os +import logging +import qrcode +from reportlab.pdfgen import canvas +from reportlab.lib.units import inch +from PyPDF2 import PdfFileReader, PdfFileWriter +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont class Common(models.Model): _name = 'sf.sync.common' @@ -92,3 +101,120 @@ class PrintingUtils(models.AbstractModel): # host = "192.168.50.110" # 可以作为参数传入,或者在此配置 # port = 9100 # 可以作为参数传入,或者在此配置 self.send_to_printer(host, port, zpl_code) + + + def add_qr_code_to_pdf(self, pdf_path:str, content:str, buttom_text:Optional[str]=False): + """ + 在PDF文件中添加二维码 + :param pdf_path: PDF文件路径 + :param content: 二维码内容 + :param buttom_text: 二维码下方文字 + :return: 是否成功 + """ + if not os.path.exists(pdf_path): + logging.warning(f'PDF文件不存在: {pdf_path}') + return False + + # 生成二维码 + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(str(content)) + qr.make(fit=True) + qr_img = qr.make_image(fill_color="black", back_color="white") + + # 保存二维码为临时文件 + qr_temp_path = '/tmp/qr_temp.png' + qr_img.save(qr_temp_path) + + # 创建一个临时PDF文件路径 + output_temp_path = '/tmp/output_temp.pdf' + + try: + # 使用reportlab创建一个新的PDF + + + # 注册中文字体 + font_paths = [ + "/usr/share/fonts/windows/simsun.ttc", # Windows系统宋体 + "c:/windows/fonts/simsun.ttc", # Windows系统宋体另一个位置 + "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", # Linux Droid字体 + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", # 文泉驿正黑 + "/usr/share/fonts/chinese/TrueType/simsun.ttc", # 某些Linux发行版位置 + ] + + font_found = False + for font_path in font_paths: + if os.path.exists(font_path): + try: + pdfmetrics.registerFont(TTFont('SimSun', font_path)) + font_found = True + break + except: + continue + + # 读取原始PDF + with open(pdf_path, "rb") as original_file: + existing_pdf = PdfFileReader(original_file) + output = PdfFileWriter() + + # 处理第一页 + page = existing_pdf.getPage(0) + # 获取页面尺寸 + page_width = float(page.mediaBox.getWidth()) + page_height = float(page.mediaBox.getHeight()) + + # 创建一个新的PDF页面用于放置二维码 + c = canvas.Canvas(output_temp_path, pagesize=(page_width, page_height)) + + # 设置字体 + if font_found: + c.setFont('SimSun', 14) # 增大字体大小到14pt + else: + # 如果没有找到中文字体,使用默认字体 + c.setFont('Helvetica', 14) + logging.warning("未找到中文字体,将使用默认字体") + + # 在右下角绘制二维码,预留边距 + qr_size = 1.5 * inch # 二维码大小为2英寸 + margin = 0.1 * inch # 边距为0.4英寸 + qr_y = margin + 20 # 将二维码向上移动一点,为文字留出空间 + c.drawImage(qr_temp_path, page_width - qr_size - margin, qr_y, width=qr_size, height=qr_size) + + if buttom_text: + # 在二维码下方绘制文字 + text = buttom_text + text_width = c.stringWidth(text, "SimSun" if font_found else "Helvetica", 14) # 准确计算文字宽度 + text_x = page_width - qr_size - margin + (qr_size - text_width) / 2 # 文字居中对齐 + text_y = margin + 20 # 文字位置靠近底部 + c.drawString(text_x, text_y, text) + + c.save() + + # 读取带有二维码的临时PDF + with open(output_temp_path, "rb") as qr_file: + qr_pdf = PdfFileReader(qr_file) + qr_page = qr_pdf.getPage(0) + + # 合并原始页面和二维码页面 + page.mergePage(qr_page) + output.addPage(page) + + # 添加剩余的页面 + for i in range(1, existing_pdf.getNumPages()): + output.addPage(existing_pdf.getPage(i)) + + # 保存最终的PDF到一个临时文件 + final_temp_path = pdf_path + '.tmp' + with open(final_temp_path, "wb") as output_file: + output.write(output_file) + + # 替换原始文件 + os.replace(final_temp_path, pdf_path) + + return True + + finally: + # 清理临时文件 + if os.path.exists(qr_temp_path): + os.remove(qr_temp_path) + if os.path.exists(output_temp_path): + os.remove(output_temp_path) \ No newline at end of file diff --git a/sf_base/views/Printer.xml b/sf_base/views/Printer.xml index 0e199b32..157121c9 100644 --- a/sf_base/views/Printer.xml +++ b/sf_base/views/Printer.xml @@ -9,6 +9,7 @@ + @@ -24,6 +25,7 @@ + diff --git a/sf_machine_connect/controllers/controllers.py b/sf_machine_connect/controllers/controllers.py index 4385937c..13394ddb 100644 --- a/sf_machine_connect/controllers/controllers.py +++ b/sf_machine_connect/controllers/controllers.py @@ -5,7 +5,7 @@ import json import base64 import logging import psycopg2 -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from odoo import http, fields from odoo.http import request @@ -414,7 +414,7 @@ class Sf_Dashboard_Connect(http.Controller): # 工单计划量切换为CNC工单 plan_data_total_counts = work_order_obj.search_count( - [('production_id.production_line_id.name', '=', line), + [('production_line_id.name', '=', line), ('id', '!=', 8061), ('state', 'in', ['ready', 'progress', 'done']), ('routing_type', '=', 'CNC加工')]) # # 工单完成量 @@ -423,13 +423,13 @@ class Sf_Dashboard_Connect(http.Controller): # 工单完成量切换为CNC工单 plan_data_finish_counts = work_order_obj.search_count( - [('production_id.production_line_id.name', '=', line), + [('production_line_id.name', '=', line), ('state', 'in', ['done']), ('routing_type', '=', 'CNC加工')]) # 超期完成量 # 搜索所有已经完成的工单 plan_data_overtime = work_order_obj.search([ - ('production_id.production_line_id.name', '=', line), + ('production_line_id.name', '=', line), ('state', 'in', ['done']), ('routing_type', '=', 'CNC加工') ]) @@ -448,9 +448,14 @@ class Sf_Dashboard_Connect(http.Controller): ]) # 过滤出那些检测结果状态为 '返工' 或 '报废' 的记录 - faulty_plans = plan_data.filtered(lambda p: any( - result.test_results in ['返工', '报废'] for result in p.production_id.detection_result_ids - )) + # faulty_plans = plan_data.filtered(lambda p: any( + # result.test_results in ['返工', '报废'] for result in p.production_id.detection_result_ids + # )) + + faulty_plans = request.env['quality.check'].sudo().search([ + ('operation_id.name', '=', 'CNC加工'), + ('quality_state', 'in', ['fail']) + ]) # 查找制造订单取消与归档的数量 cancel_order_count = production_obj.search_count( @@ -567,7 +572,7 @@ class Sf_Dashboard_Connect(http.Controller): """ res = {'status': 1, 'message': '成功', 'data': {}} # plan_obj = request.env['sf.production.plan'].sudo() - plan_obj = request.env['mrp.workorder'].sudo().search([('routing_type', '=', 'CNC加工')]) + # plan_obj = request.env['mrp.workorder'].sudo().search([('routing_type', '=', 'CNC加工')]) line_list = ast.literal_eval(kw['line_list']) begin_time_str = kw['begin_time'].strip('"') end_time_str = kw['end_time'].strip('"') @@ -617,11 +622,19 @@ class Sf_Dashboard_Connect(http.Controller): for time_interval in time_intervals: start_time, end_time = time_interval - orders = plan_obj.search([ - ('production_id.production_line_id.name', '=', line), + # orders = plan_obj.search([ + # ('production_line_id.name', '=', line), + # ('state', 'in', ['done']), + # (date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')), + # (date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) # 包括结束时间 + # ]) + + orders = request.env['mrp.workorder'].sudo().search([ + ('routing_type', '=', 'CNC加工'), # 将第一个条件合并进来 + ('production_line_id.name', '=', line), ('state', 'in', ['done']), (date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')), - (date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) # 包括结束时间 + (date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) ]) # 使用小时和分钟作为键,确保每个小时的数据有独立的键 @@ -638,18 +651,22 @@ class Sf_Dashboard_Connect(http.Controller): for date in date_list: next_day = date + timedelta(days=1) - orders = plan_obj.search([('production_id.production_line_id.name', '=', line), ('state', 'in', ['done']), - (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_id.production_line_id.name', '=', line), ('state', 'in', ['rework']), + orders = request.env['mrp.workorder'].sudo().search( + [('production_id.production_line_id.name', '=', line), ('state', 'in', ['done']), + ('routing_type', '=', 'CNC加工'), (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( + + rework_orders = request.env['mrp.workorder'].sudo().search( + [('production_id.production_line_id.name', '=', line), ('state', 'in', ['rework']), + ('routing_type', '=', 'CNC加工'), + (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 = request.env['mrp.workorder'].sudo().search( [('production_id.production_line_id.name', '=', line), ('state', 'in', ['scrap', 'cancel']), + ('routing_type', '=', 'CNC加工'), (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) ]) @@ -751,11 +768,14 @@ class Sf_Dashboard_Connect(http.Controller): for line in line_list: # 未完成订单 - not_done_orders = plan_obj.search( - [('production_line_id.name', '=', line), ('state', 'not in', ['finished']), - ('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True) + # not_done_orders = plan_obj.search( + # [('production_line_id.name', '=', line), ('state', 'not in', ['finished']), + # ('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True) + # ]) + not_done_orders = request.env['mrp.workorder'].sudo().search( + [('production_line_id.name', '=', line), ('state', 'in', ['ready', 'progress']), + ('routing_type', '=', 'CNC加工') ]) - # print(not_done_orders) # 完成订单 # 获取当前时间,并计算24小时前的时间 @@ -807,16 +827,18 @@ class Sf_Dashboard_Connect(http.Controller): 'draft': '待排程', 'done': '已排程', 'processing': '生产中', - 'finished': '已完成' + 'finished': '已完成', + 'ready': '待加工', + 'progress': '生产中', } line_dict = { 'sequence': id_to_sequence[order.id], - 'workorder_name': order.name, + 'workorder_name': order.production_id.name, 'blank_name': blank_name, 'material': material, 'dimensions': dimensions, - 'order_qty': order.product_qty, + 'order_qty': 1, 'state': state_dict[order.state], } @@ -897,15 +919,17 @@ class Sf_Dashboard_Connect(http.Controller): cur.execute(sql2, (item,)) result2 = cur.fetchall() - # print('result2========', result2) - # + for row in result: res['data'][item] = {'idle_count': row[0]} alarm_count = [] for row in result2: alarm_count.append(row[1]) if row[0]: - total_alarm_time += abs(float(row[0])) + if float(row[0]) >= 28800: + continue + # total_alarm_time += abs(float(row[0])) + total_alarm_time += float(row[0]) else: total_alarm_time += 0.0 if len(list(set(alarm_count))) == 1: @@ -915,6 +939,7 @@ class Sf_Dashboard_Connect(http.Controller): alarm_count_num = 1 else: alarm_count_num = len(list(set(alarm_count))) + res['data'][item]['total_alarm_time'] = total_alarm_time / 3600 res['data'][item]['alarm_count_num'] = alarm_count_num @@ -1332,7 +1357,7 @@ class Sf_Dashboard_Connect(http.Controller): for result in results: alarm_last_24_nums.append(result[1]) if result[0]: - if float(result[0]) >= 1000: + if float(result[0]) >= 28800: continue alarm_last_24_time += float(result[0]) else: @@ -1350,7 +1375,7 @@ class Sf_Dashboard_Connect(http.Controller): for result in results: alarm_all_nums.append(result[1]) if result[0]: - if float(result[0]) >= 1000: + if float(result[0]) >= 28800: continue alarm_all_time += float(result[0]) else: @@ -1385,3 +1410,207 @@ class Sf_Dashboard_Connect(http.Controller): conn.close() return json.dumps(res) + + @http.route('/api/utilization/rate', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def UtilizationRate(self, **kw): + """ + 获取稼动率 + """ + logging.info("kw=:%s" % kw) + res = {'status': 1, 'message': '成功', 'data': {}} + # 获取请求的机床数据 + machine_list = ast.literal_eval(kw['machine_list']) + line = kw['line'] + orders = request.env['mrp.workorder'].sudo().search([ + ('routing_type', '=', 'CNC加工'), # 将第一个条件合并进来 + ('production_line_id.name', '=', line), + ('state', 'in', ['done']) + ]) + + faulty_plans = request.env['quality.check'].sudo().search([ + ('operation_id.name', '=', 'CNC加工'), + ('quality_state', 'in', ['fail']) + ]) + + # 计算时间范围 + now = datetime.now() + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + + total_power_on_time = 0 + month_power_on_time = 0 + today_power_on_time = 0 + today_power_on_dict = {} + + today_data = [] + month_data = [] + today_check_ng = [] + month_check_ng = [] + + total_alarm_time = 0 + today_alarm_time = 0 + month_alarm_time = 0 + + for order in orders: + time = datetime.strptime(order.date_finished, "%Y-%m-%d %H:%M:%S") + if time >= today_start: + today_data.append(order) + if time >= month_start: + month_data.append(order) + + for faulty_plan in faulty_plans: + time = faulty_plan.write_date + if time >= today_start: + today_check_ng.append(faulty_plan) + if time >= month_start: + month_check_ng.append(faulty_plan) + + # 连接数据库 + conn = psycopg2.connect(**db_config) + for item in machine_list: + with conn.cursor() as cur: + cur.execute(""" + ( + SELECT power_on_time, 'latest' AS record_type + FROM device_data + WHERE device_name = %s + AND power_on_time IS NOT NULL + ORDER BY time DESC + LIMIT 1 + ) + UNION ALL + ( + SELECT power_on_time, 'month_first' AS record_type + FROM device_data + WHERE device_name = %s + AND power_on_time IS NOT NULL + AND time >= date_trunc('month', CURRENT_DATE) -- ✅ 修复日期函数 + AND time < (date_trunc('month', CURRENT_DATE) + INTERVAL '1 month')::date + ORDER BY time ASC + LIMIT 1 + ) + UNION ALL + ( + SELECT power_on_time, 'day_first' AS record_type + FROM device_data + WHERE device_name = %s + AND power_on_time IS NOT NULL + AND time::date = CURRENT_DATE -- ✅ 更高效的写法 + ORDER BY time ASC + LIMIT 1 + ); + """, (item, item, item)) + results = cur.fetchall() + print(results) + if len(results) >= 1: + total_power_on_time += convert_to_seconds(results[0][0]) + else: + total_power_on_time += 0 + if len(results) >= 2: + month_power_on_time += convert_to_seconds(results[1][0]) + else: + month_power_on_time += 0 + if len(results) >= 3: + today_power_on_time += convert_to_seconds(results[2][0]) + today_power_on_dict[item] = today_power_on_time + else: + today_power_on_time += 0 + print(total_power_on_time, month_power_on_time, today_power_on_time) + + with conn.cursor() as cur: + cur.execute(""" + SELECT DISTINCT ON (alarm_start_time) alarm_time, alarm_start_time + FROM device_data + WHERE device_name = %s AND alarm_start_time IS NOT NULL + ORDER BY alarm_start_time, time; + """, (item,)) + results = cur.fetchall() + today_data = [] + month_data = [] + + for record in results: + if record[0]: + if float(record[0]) >= 28800: + continue + total_alarm_time += float(record[0]) + else: + total_alarm_time += 0.0 + alarm_start = datetime.strptime(record[1], "%Y-%m-%d %H:%M:%S") + if alarm_start >= today_start: + today_data.append(record) + if alarm_start >= month_start: + month_data.append(record) + for today in today_data: + if today[0]: + if float(today[0]) >= 28800: + continue + today_alarm_time += float(today[0]) + else: + today_alarm_time += 0.0 + for month in month_data: + if month[0]: + if float(month[0]) >= 28800: + continue + month_alarm_time += float(month[0]) + else: + month_alarm_time += 0.0 + + conn.close() + + print('报警时间=============', total_alarm_time, month_alarm_time, today_alarm_time) + logging.info("报警时间=%s" % total_alarm_time) + logging.info("报警时间=%s" % month_alarm_time) + logging.info("报警时间=%s" % today_alarm_time) + # 计算时间开动率(累计、月、日) + if total_power_on_time: + total_power_on_rate = (total_power_on_time - total_alarm_time) / total_power_on_time + else: + total_power_on_rate = 0 + if month_power_on_time: + month_power_on_rate = (total_power_on_time - month_power_on_time - month_alarm_time) / month_power_on_time + else: + month_power_on_rate = 0 + if today_power_on_time: + today_power_on_rate = (total_power_on_time - today_power_on_time - today_alarm_time) / today_power_on_time + else: + today_power_on_rate = 0 + print("总开动率: %s" % total_power_on_rate) + print("月开动率: %s" % month_power_on_rate) + print("日开动率: %s" % today_power_on_rate) + + # 计算性能开动率(累计、月、日) + print('===========',orders) + print(len(orders)) + total_performance_rate = len(orders) * 30 * 60 / (total_power_on_time - total_alarm_time) + month_performance_rate = len(month_data) * 30 * 60 / (month_power_on_time - month_alarm_time) + today_performance_rate = len(today_data) * 30 * 60 / (today_power_on_time - today_alarm_time) if today_power_on_time != 0 else 0 + print("总性能率: %s" % total_performance_rate) + print("月性能率: %s" % month_performance_rate) + print("日性能率: %s" % today_performance_rate) + + # 计算累计合格率 + total_pass_rate = (len(orders) - len(today_check_ng)) / len(orders) if len(orders) != 0 else 0 + month_pass_rate = (len(month_data) - len(month_check_ng)) / len(month_data) if len(month_data) != 0 else 0 + today_pass_rate = (len(today_data) - len(today_check_ng)) / len(today_data) if len(today_data) != 0 else 0 + print("总合格率: %s" % total_pass_rate) + print("月合格率: %s" % month_pass_rate) + print("日合格率: %s" % today_pass_rate) + + # # 返回数据 + # res['data'][item] = { + # 'total_utilization_rate': total_power_on_rate * total_performance_rate * total_pass_rate, + # 'month_utilization_rate': month_power_on_rate * month_performance_rate * month_pass_rate, + # 'today_utilization_rate': today_power_on_rate * today_performance_rate * today_pass_rate, + # } + res['data'] = { + 'total_utilization_rate': total_power_on_rate * total_performance_rate * total_pass_rate, + 'month_utilization_rate': month_power_on_rate * month_performance_rate * month_pass_rate, + 'today_utilization_rate': today_power_on_rate * today_performance_rate * today_pass_rate, + } + + return json.dumps(res) + + + + + diff --git a/sf_machine_connect/models/ftp_operate.py b/sf_machine_connect/models/ftp_operate.py index 29ee69a1..6cc29c74 100644 --- a/sf_machine_connect/models/ftp_operate.py +++ b/sf_machine_connect/models/ftp_operate.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging -from ftplib import FTP +import os +from ftplib import FTP, error_perm _logger = logging.getLogger(__name__) @@ -52,8 +53,8 @@ class FtpController: print(self.username, self.port, self.host, self.password) ftp = FTP_P() _logger.info("===================connect==================") - # self.ftp.set_debuglevel(2) #打开调试级别2,显示详细信息 - ftp.set_pasv(0) # 0主动模式 1 #被动模式 + # ftp.set_debuglevel(2) #打开调试级别2,显示详细信息 + # ftp.set_pasv(1) # 0主动模式 1 #被动模式 try: ftp.connect(self.host, self.port) ftp.login(self.username, self.password) @@ -128,3 +129,126 @@ class FtpController: :return: """ self.ftp.delete(delpath) + + + +def transfer_nc_files(source_ftp_info, target_ftp_info, source_dir, target_dir, keep_dir=False): + """ + 从源FTP服务器下载所有.nc文件并上传到目标FTP服务器,保持目录结构 + + Args: + source_ftp_info: dict, 源FTP连接信息 {host, port, username, password} + target_ftp_info: dict, 目标FTP连接信息 {host, port, username, password} + source_dir: str, 源FTP上的起始目录 + target_dir: str, 目标FTP上的目标目录 + keep_dir: bool, 是否保持目录结构,默认False + """ + try: + # 连接源FTP + source_ftp = FtpController( + source_ftp_info['host'], + source_ftp_info['port'], + source_ftp_info['username'], + source_ftp_info['password'] + ) + source_ftp.ftp.set_pasv(1) + + # 连接目标FTP + target_ftp = FtpController( + target_ftp_info['host'], + target_ftp_info['port'], + target_ftp_info['username'], + target_ftp_info['password'] + ) + source_ftp.ftp.set_pasv(1) + + # 递归遍历源目录 + def traverse_dir(current_dir, relative_path=''): + source_ftp.ftp.cwd(current_dir) + file_list = source_ftp.ftp.nlst() + + for item in file_list: + try: + # 尝试进入目录 + source_ftp.ftp.cwd(f"{current_dir}/{item}") + # 如果成功则是目录 + new_relative_path = os.path.join(relative_path, item) + # 在目标FTP创建对应目录 + try: + if keep_dir: + target_ftp.ftp.mkd(f"{target_dir}/{new_relative_path}") + except: + pass # 目录可能已存在 + # 递归遍历子目录 + traverse_dir(f"{current_dir}/{item}", new_relative_path) + source_ftp.ftp.cwd('..') + except: + # 如果是.nc文件则传输 + if item.lower().endswith('.nc'): + # 下载到临时文件 + temp_path = f"/tmp/{item}" + with open(temp_path, 'wb') as f: + source_ftp.ftp.retrbinary(f'RETR {item}', f.write) + + # 上传到目标FTP对应目录 + if keep_dir: + target_path = f"{target_dir}/{relative_path}/{item}" + else: + target_path = f"{target_dir}/{item}" + with open(temp_path, 'rb') as f: + target_ftp.ftp.storbinary(f'STOR {target_path}', f) + + # 删除临时文件 + os.remove(temp_path) + logging.info(f"已传输文件: {item}") + + # 清空目标目录下的所有内容 + try: + target_ftp.ftp.cwd(target_dir) + files = target_ftp.ftp.nlst() + + for f in files: + try: + # 尝试删除文件 + target_ftp.ftp.delete(f) + except: + try: + # 如果删除失败,可能是目录,递归删除目录 + def remove_dir(path): + target_ftp.ftp.cwd(path) + sub_files = target_ftp.ftp.nlst() + for sf in sub_files: + try: + target_ftp.ftp.delete(sf) + except: + remove_dir(f"{path}/{sf}") + target_ftp.ftp.cwd('..') + target_ftp.ftp.rmd(path) + + remove_dir(f"{target_dir}/{f}") + except: + logging.error(f"无法删除 {f}") + pass + + logging.info(f"已清空目标目录 {target_dir}") + except Exception as e: + logging.error(f"清空目标目录失败: {str(e)}") + return False + + # 开始遍历 + traverse_dir(source_dir) + + logging.info("所有.nc文件传输完成") + return True + + except Exception as e: + logging.error(f"传输过程出错: {str(e)}") + return False + + finally: + # 关闭FTP连接 + try: + source_ftp.ftp.quit() + target_ftp.ftp.quit() + except: + pass \ No newline at end of file diff --git a/sf_maintenance/models/__init__.py b/sf_maintenance/models/__init__.py index b177eb3c..4e925ec6 100644 --- a/sf_maintenance/models/__init__.py +++ b/sf_maintenance/models/__init__.py @@ -4,3 +4,4 @@ from . import sf_maintenance_oee from . import sf_maintenance_logs from . import sf_equipment_maintenance_standards from . import sf_maintenance_requests +from . import maintenance_printer diff --git a/sf_maintenance/models/maintenance_printer.py b/sf_maintenance/models/maintenance_printer.py new file mode 100644 index 00000000..b2c251aa --- /dev/null +++ b/sf_maintenance/models/maintenance_printer.py @@ -0,0 +1,92 @@ +import qrcode +import base64 +from io import BytesIO +from odoo import models, fields, api + +class MaintenanceEquipment(models.Model): + _name = 'maintenance.equipment' + _inherit = ['maintenance.equipment', 'printing.utils'] + + qr_code_image = fields.Binary(string='二维码', compute='_generate_qr_code') + + @api.depends('name') + def _generate_qr_code(self): + for record in self: + # Generate QR code + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(record.name) + qr.make(fit=True) + qr_image = qr.make_image(fill_color="black", back_color="white") + + # Encode the image data in base64 + image_stream = BytesIO() + qr_image.save(image_stream, format="PNG") + encoded_image = base64.b64encode(image_stream.getvalue()) + + record.qr_code_image = encoded_image + + def print_single_method(self): + + print('self.name========== %s' % self.name) + self.ensure_one() + qr_code_data = self.qr_code_image + if not qr_code_data: + raise UserError("没有找到二维码数据。") + maintenance_equipment_name = self.name + # host = "192.168.50.110" # 可以根据实际情况修改 + # port = 9100 # 可以根据实际情况修改 + + # 获取默认打印机配置 + printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1) + if not printer_config: + raise UserError('请先配置打印机') + host = printer_config.printer_id.ip_address + port = printer_config.printer_id.port + self.print_qr_code(maintenance_equipment_name, host, port) + + + def generate_zpl_code(self, code): + """生成ZPL代码用于打印二维码标签 + Args: + code: 需要编码的内容 + Returns: + str: ZPL指令字符串 + """ + zpl_code = "^XA\n" # 开始ZPL格式 + + # 设置打印参数 + zpl_code += "^LH0,0\n" # 设置标签起始位置 + zpl_code += "^CI28\n" # 设置中文编码 + zpl_code += "^PW400\n" # 设置打印宽度为400点 + zpl_code += "^LL300\n" # 设置标签长度为300点 + + # 打印标题 + zpl_code += "^FO10,20\n" # 设置标题位置 + zpl_code += "^A0N,30,30\n" # 设置字体大小 + zpl_code += "^FD机床二维码^FS\n" # 打印标题文本 + + # 打印二维码 + zpl_code += "^FO50,60\n" # 设置二维码位置 + zpl_code += f"^BQN,2,8\n" # 设置二维码参数:模式2,放大倍数8 + zpl_code += f"^FDLA,{code}^FS\n" # 二维码内容 + + # 打印编码文本 + zpl_code += "^FO50,220\n" # 设置编码文本位置 + zpl_code += "^A0N,25,25\n" # 设置字体大小 + zpl_code += f"^FD编码: {code}^FS\n" # 打印编码文本 + + # 打印日期 + zpl_code += "^FO50,260\n" + zpl_code += "^A0N,20,20\n" + zpl_code += f"^FD打印日期: {fields.Date.today()}^FS\n" + + zpl_code += "^PQ1\n" # 打印1份 + zpl_code += "^XZ\n" # 结束ZPL格式 + + return zpl_code + diff --git a/sf_maintenance/models/sf_maintenance.py b/sf_maintenance/models/sf_maintenance.py index de6be8da..b7c38916 100644 --- a/sf_maintenance/models/sf_maintenance.py +++ b/sf_maintenance/models/sf_maintenance.py @@ -826,6 +826,11 @@ class SfMaintenanceEquipment(models.Model): image_lq_id = fields.Many2many('maintenance.equipment.image', 'equipment_lq_id', string='冷却方式', domain="[('type', '=', '冷却方式')]") + ftp_host = fields.Char('FTP 主机') + ftp_port = fields.Char('FTP 端口') + ftp_username = fields.Char('FTP 用户名') + ftp_password = fields.Char('FTP 密码') + class SfRobotAxisNum(models.Model): _name = 'sf.robot.axis.num' diff --git a/sf_maintenance/views/maintenance_views.xml b/sf_maintenance/views/maintenance_views.xml index 4fe044d3..381319a6 100644 --- a/sf_maintenance/views/maintenance_views.xml +++ b/sf_maintenance/views/maintenance_views.xml @@ -1053,6 +1053,26 @@ + + + + + + + + + + + + + + + diff --git a/sf_manufacturing/__manifest__.py b/sf_manufacturing/__manifest__.py index 817f75b9..24c8a4b0 100644 --- a/sf_manufacturing/__manifest__.py +++ b/sf_manufacturing/__manifest__.py @@ -47,6 +47,7 @@ 'views/sale_order_views.xml', 'views/mrp_workorder_batch_replan.xml', 'views/purchase_order_view.xml', + 'views/product_template_views.xml', ], 'assets': { diff --git a/sf_manufacturing/models/__init__.py b/sf_manufacturing/models/__init__.py index 93e2c39b..9bad61c6 100644 --- a/sf_manufacturing/models/__init__.py +++ b/sf_manufacturing/models/__init__.py @@ -17,4 +17,5 @@ from . import sale_order from . import quick_easy_order from . import purchase_order from . import quality_check -from . import purchase_request_line \ No newline at end of file +from . import purchase_request_line +from . import workorder_printer diff --git a/sf_manufacturing/models/workorder_printer.py b/sf_manufacturing/models/workorder_printer.py new file mode 100644 index 00000000..46eb3b8b --- /dev/null +++ b/sf_manufacturing/models/workorder_printer.py @@ -0,0 +1,134 @@ +import qrcode +import base64 +import logging +import tempfile +import os +import platform +import socket +import subprocess +from io import BytesIO +from odoo import models, fields, api +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +class MrpWorkorder(models.Model): + _name = 'mrp.workorder' + _inherit = ['mrp.workorder', 'printing.utils'] + + def print_pdf(self, printer_config, pdf_data): + """跨平台打印函数,支持网络打印机(IP:端口)""" + # 将PDF数据保存到临时文件 + with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file: + pdf_binary = base64.b64decode(pdf_data) + temp_file.write(pdf_binary) + temp_file_path = temp_file.name + + _logger.info(f"开始打印PDF文件: {temp_file_path}") + + try: + # 获取打印机名称或IP地址 + printer_name = printer_config.printer_id.name + if not printer_name: + raise UserError('打印机名称未配置') + + # 使用打印机配置中的IP地址和端口 + printer_ip = printer_config.printer_id.ip_address + printer_port = printer_config.printer_id.port + _logger.info(f"使用网络打印机: IP={printer_ip}, 端口={printer_port}") + + if platform.system() == 'Windows': + _logger.info(f"Windows环境不支持网络打印机") + else: # Linux环境 + # try: + # import cups + + # # 处理网络打印机情况 + # _logger.info(f"Linux环境下连接网络打印机: {printer_ip}:{printer_port}") + + # # 创建连接 + # conn = cups.Connection() + + # # 检查打印机是否已经添加到系统 + # printers = conn.getPrinters() + # _logger.info(f"可用打印机列表: {list(printers.keys())}") + + # network_printer_name = f"IP_{printer_ip}_{printer_port}" + + # # 如果打印机不存在,尝试添加 + # if network_printer_name not in printers: + # _logger.info(f"添加网络打印机: {network_printer_name}") + # conn.addPrinter( + # network_printer_name, + # device=f"socket://{printer_ip}:{printer_port}", + # info=f"Network Printer {printer_ip}:{printer_port}", + # location="Network" + # ) + # # 设置打印机为启用状态 + # conn.enablePrinter(network_printer_name) + # _logger.info(f"网络打印机添加成功: {network_printer_name}") + + # # 打印文件 + # _logger.info(f"开始打印到网络打印机: {network_printer_name}") + # job_id = conn.printFile(network_printer_name, temp_file_path, "工单打印", {}) + # _logger.info(f"打印作业ID: {job_id}") + + + # except ImportError as ie: + # _logger.error(f"导入CUPS库失败: {str(ie)}") + + # 尝试使用lp命令打印 + try: + _logger.info("尝试使用lp命令打印...") + + # 使用socket设置打印 + cmd = f"lp -h {printer_ip}:{printer_port} -d {printer_name} {temp_file_path}" + + _logger.info(f"执行lp打印命令: {cmd}") + result = subprocess.run(cmd, shell=True, check=True, capture_output=True) + _logger.info(f"lp打印结果: {result.stdout.decode()}") + except Exception as e: + _logger.error(f"lp命令打印失败: {str(e)}") + raise UserError(f'打印失败,请安装cups打印库: pip install pycups 或确保lp命令可用') + + return True + except Exception as e: + _logger.error(f"打印失败详细信息: {str(e)}") + raise UserError(f'打印失败: {str(e)}') + finally: + # 清理临时文件 + if os.path.exists(temp_file_path): + try: + os.unlink(temp_file_path) + _logger.info(f"临时文件已清理: {temp_file_path}") + except Exception as e: + _logger.error(f"清理临时文件失败: {str(e)}") + + def _compute_state(self): + super(MrpWorkorder, self)._compute_state() + for workorder in self: + work_ids = workorder.production_id.workorder_ids.filtered(lambda w: w.routing_type == '装夹预调' or w.routing_type == '人工线下加工') + for wo in work_ids: + if wo.state == 'ready' and not wo.production_id.product_id.is_print_program: + # 触发打印程序 + pdf_data = self.processing_drawing + try: + if pdf_data: + # 获取默认打印机配置 + printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name), ('printer_id.type', '=', 'normal')], limit=1) + if not printer_config: + raise UserError('请先配置打印机') + + # 执行打印 + if self.print_pdf(printer_config, pdf_data): + wo.production_id.product_id.is_print_program = True + _logger.info(f"工单 {wo.name} 的PDF已成功打印") + + except Exception as e: + _logger.error(f'打印配置错误: {str(e)}') + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + is_print_program = fields.Boolean(string='是否打印程序', default=False) + diff --git a/sf_manufacturing/views/product_template_views.xml b/sf_manufacturing/views/product_template_views.xml new file mode 100644 index 00000000..a99a332a --- /dev/null +++ b/sf_manufacturing/views/product_template_views.xml @@ -0,0 +1,15 @@ + + + + + product.template.product.form.inherit.sf_manufacture + product.template + + + + + + + + + \ No newline at end of file diff --git a/sf_mrs_connect/controllers/controllers.py b/sf_mrs_connect/controllers/controllers.py index 7599ed6c..c30d3d53 100644 --- a/sf_mrs_connect/controllers/controllers.py +++ b/sf_mrs_connect/controllers/controllers.py @@ -8,6 +8,7 @@ from odoo.http import request from odoo.addons.sf_base.controllers.controllers import MultiInheritController + class Sf_Mrs_Connect(http.Controller, MultiInheritController): @http.route('/api/cnc_processing/create', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, @@ -22,6 +23,7 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController): try: res = {'status': 1, 'message': '成功'} datas = request.httprequest.data + model_id = None ret = json.loads(datas) ret = json.loads(ret['result']) logging.info('下发编程单:%s' % ret) @@ -57,6 +59,7 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController): res['message'] = '编程单号为%s的CNC程序文件从FTP拉取失败' % (ret['programming_no']) return json.JSONEncoder().encode(res) for production in productions: + model_id = production.product_id.model_id # 一个编程单的制造订单对应同一个模型 production.write({'programming_state': '已编程', 'work_state': '已编程', 'is_rework': False}) for panel in ret['processing_panel'].split(','): # 查询状态为进行中且工序类型为CNC加工的工单 @@ -83,19 +86,23 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController): # panel) program_path_tmp_panel = os.path.join('/tmp', ret['folder_name'], 'return', panel) files_panel = os.listdir(program_path_tmp_panel) + panel_file_path = '' if files_panel: for file in files_panel: file_extension = os.path.splitext(file)[1] if file_extension.lower() == '.pdf': panel_file_path = os.path.join(program_path_tmp_panel, file) logging.info('panel_file_path:%s' % panel_file_path) - cnc_workorder.write({'cnc_worksheet': base64.b64encode(open(panel_file_path, 'rb').read())}) - pre_workorder = productions.workorder_ids.filtered( - lambda ap: ap.routing_type in ['装夹预调', '人工线下加工'] and ap.state not in ['done', 'rework' - 'cancel'] and ap.processing_panel == panel) - if pre_workorder: - pre_workorder.write( - {'processing_drawing': base64.b64encode(open(panel_file_path, 'rb').read())}) + + # 向编程单中添加二维码 + request.env['printing.utils'].add_qr_code_to_pdf(panel_file_path, model_id, "扫码获取工单") + cnc_workorder.write({'cnc_worksheet': base64.b64encode(open(panel_file_path, 'rb').read())}) + pre_workorder = productions.workorder_ids.filtered( + lambda ap: ap.routing_type in ['装夹预调', '人工线下加工'] and ap.state not in ['done', 'rework' + 'cancel'] and ap.processing_panel == panel) + if pre_workorder: + pre_workorder.write( + {'processing_drawing': base64.b64encode(open(panel_file_path, 'rb').read())}) productions.write({'programming_state': '已编程', 'work_state': '已编程'}) productions.filtered(lambda p: p.production_type == '人工线下加工').write({'manual_quotation': True}) logging.info('已更新制造订单编程状态:%s' % productions.ids) @@ -268,3 +275,6 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController): request.cr.rollback() logging.info('get_cnc_processing_create error:%s' % e) return json.JSONEncoder().encode(res) + + + \ No newline at end of file diff --git a/sf_mrs_connect/data/ir_cron_data.xml b/sf_mrs_connect/data/ir_cron_data.xml index 7cc18c8e..a2d43891 100644 --- a/sf_mrs_connect/data/ir_cron_data.xml +++ b/sf_mrs_connect/data/ir_cron_data.xml @@ -1,16 +1,16 @@ - - - 制造-配置:每日定时同步cloud的静态资源库 - - code - model.sf_all_sync() - 1 - days - -1 - - + + + 制造-配置:每日定时同步cloud的静态资源库 + + code + model.sf_all_sync() + 1 + days + -1 + + @@ -220,4 +220,5 @@ + \ No newline at end of file diff --git a/sf_quality/data/insepection_report_template.xml b/sf_quality/data/insepection_report_template.xml index d989137c..14a42065 100644 --- a/sf_quality/data/insepection_report_template.xml +++ b/sf_quality/data/insepection_report_template.xml @@ -78,6 +78,10 @@

公司邮箱:

+ +
+ 1 页/共 1 +