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_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_manufacturing/models/agv_scheduling.py b/sf_manufacturing/models/agv_scheduling.py index cc621727..33522182 100644 --- a/sf_manufacturing/models/agv_scheduling.py +++ b/sf_manufacturing/models/agv_scheduling.py @@ -87,11 +87,12 @@ class AgvScheduling(models.Model): agv_route_type: AGV任务类型 workorders: 工单 """ + scheduling = None _logger.info('创建AGV调度任务\r\n起点为【%s】,任务类型为【%s】,工单为【%s】' % (agv_start_site_name, agv_route_type, workorders)) if not workorders: raise UserError(_('工单不能为空')) - agv_start_site = self.env['sf.agv.site'].sudo().search([('name', '=', agv_start_site_name)], limit=1) - if not agv_start_site: + agv_start_sites = self.env['sf.agv.site'].sudo().search([('name', '=', agv_start_site_name)]) + if not agv_start_sites: raise UserError(_('不存在名称为【%s】的接驳站,请先创建!' % agv_start_site_name)) # 如果存在相同任务类型工单的AGV调度任务,则提示错误 agv_scheduling = self.sudo().search([ @@ -107,24 +108,32 @@ class AgvScheduling(models.Model): (','.join(repetitive_workorders.mapped('production_id.name')), agv_scheduling.name) ) + # 如果只有唯一任务路线,则自动赋予终点接驳站跟任务名称 + agv_routes = self.env['sf.agv.task.route'].sudo().search([ + ('route_type', '=', agv_route_type), + ('start_site_id', 'in', agv_start_sites.ids) + ]) vals = { - 'start_site_id': agv_start_site.id, 'agv_route_type': agv_route_type, 'workorder_ids': workorders.ids, # 'workpiece_delivery_ids': deliveries.mapped('id') if deliveries else [], 'task_create_time': fields.Datetime.now() } - # 如果只有唯一任务路线,则自动赋予终点接驳站跟任务名称 - agv_routes = self.env['sf.agv.task.route'].sudo().search([ - ('route_type', '=', agv_route_type), - ('start_site_id', '=', agv_start_site.id) - ]) if not agv_routes: raise UserError(_('不存在起点为【%s】的【%s】任务路线,请先创建!' % (agv_start_site_name, agv_route_type))) + # 如果路线中包含起点与终点相同的接驳站,则不创建AGV调度任务 + if agv_routes.filtered(lambda r: r.start_site_id.name == r.end_site_id.name): + return True + # 配送类型相同的接驳站为同一个,取第一个即可 + vals.update({ + 'start_site_id': agv_routes[0].start_site_id.id, + }) idle_route = None if len(agv_routes) == 1: idle_route = agv_routes[0] - vals.update({'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id}) + vals.update({ + 'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id + }) else: # 判断终点接驳站是否为空闲 idle_routes = agv_routes.filtered(lambda r: r.end_site_id.state == '空闲') @@ -132,7 +141,10 @@ class AgvScheduling(models.Model): # 将空闲的路线按照终点接驳站名称排序 idle_routes = sorted(idle_routes, key=lambda r: r.end_site_id.name) idle_route = idle_routes[0] - vals.update({'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id}) + vals.update({ + 'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id + }) + try: scheduling = self.env['sf.agv.scheduling'].sudo().create(vals) # 触发空闲接驳站状态更新,触发新任务下发 @@ -142,7 +154,7 @@ class AgvScheduling(models.Model): except Exception as e: _logger.error('添加AGV调度任务失败: %s', e) raise UserError(_('添加AGV调度任务失败: %s', e)) - + return scheduling def on_site_state_change(self, agv_site_id, agv_site_state): diff --git a/sf_manufacturing/models/agv_setting.py b/sf_manufacturing/models/agv_setting.py index 820ed539..d33ea9b4 100644 --- a/sf_manufacturing/models/agv_setting.py +++ b/sf_manufacturing/models/agv_setting.py @@ -24,7 +24,7 @@ class AgvSetting(models.Model): # name必须唯一 _sql_constraints = [ - ('name_uniq', 'unique (name)', '站点编号必须唯一!'), + ('name_uniq', 'unique (name, workcenter_id)', '同一工作中心的站点编号必须唯一!'), ] # def update_site_state(self): @@ -68,11 +68,12 @@ class AgvSetting(models.Model): """ if isinstance(agv_site_state_arr, dict): for agv_site_name, is_occupy in agv_site_state_arr.items(): - agv_site = self.env['sf.agv.site'].sudo().search([('name', '=', agv_site_name)]) - if agv_site: - agv_site.state = is_occupy + agv_sites = self.env['sf.agv.site'].sudo().search([('name', '=', agv_site_name)]) + if agv_sites: + agv_sites.state = is_occupy if notify: - self.env['sf.agv.scheduling'].on_site_state_change(agv_site.id, agv_site.state) + for agv_site in agv_sites: + self.env['sf.agv.scheduling'].on_site_state_change(agv_site.id, agv_site.state) else: _logger.error("更新失败:接驳站站点错误!%s" % agv_site_name) raise UserError("更新失败:接驳站站点错误!") diff --git a/sf_manufacturing/views/agv_setting_views.xml b/sf_manufacturing/views/agv_setting_views.xml index 62e3c623..b289f4b6 100644 --- a/sf_manufacturing/views/agv_setting_views.xml +++ b/sf_manufacturing/views/agv_setting_views.xml @@ -15,6 +15,23 @@ + + agv.site.form + sf.agv.site + +
+ + + + + + + + +
+
+
+ AGV站点 sf.agv.site @@ -39,7 +56,8 @@ - + diff --git a/sf_manufacturing/wizard/workpiece_delivery_wizard.py b/sf_manufacturing/wizard/workpiece_delivery_wizard.py index c41a7619..b100325e 100644 --- a/sf_manufacturing/wizard/workpiece_delivery_wizard.py +++ b/sf_manufacturing/wizard/workpiece_delivery_wizard.py @@ -117,17 +117,30 @@ class WorkpieceDeliveryWizard(models.TransientModel): item.button_finish() # return scheduling.read()[0] - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'target': 'new', - 'params': { - 'message': f'任务下发成功!AGV任务调度编号为【{scheduling.name}】', - 'type': 'success', - 'sticky': False, - 'next': {'type': 'ir.actions.act_window_close'}, + if isinstance(scheduling, bool) and scheduling is True: + return{ + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'target': 'new', + 'params': { + 'message': f'解除装夹成功', + 'type': 'success', + 'sticky': False, + 'next': {'type': 'ir.actions.act_window_close'}, + } + } + else: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'target': 'new', + 'params': { + 'message': f'任务下发成功!AGV任务调度编号为【{scheduling.name}】', + 'type': 'success', + 'sticky': False, + 'next': {'type': 'ir.actions.act_window_close'}, + } } - } except Exception as e: logging.info('%s任务下发失败:%s', self.delivery_type, e) raise UserError(f'{self.delivery_type}任务下发失败:{e}') from e diff --git a/sf_quality/data/insepection_report_template.xml b/sf_quality/data/insepection_report_template.xml index afe7148b..14a42065 100644 --- a/sf_quality/data/insepection_report_template.xml +++ b/sf_quality/data/insepection_report_template.xml @@ -61,6 +61,29 @@ + + + + + \ No newline at end of file diff --git a/sf_quality/data/report_actions.xml b/sf_quality/data/report_actions.xml index 7ee44f63..59338103 100644 --- a/sf_quality/data/report_actions.xml +++ b/sf_quality/data/report_actions.xml @@ -18,7 +18,7 @@ 预览检验报告 quality.check qweb-html - sf_quality.report_quality_inspection + sf_quality.html_report_quality_inspection report \ No newline at end of file diff --git a/sf_tool_management/__manifest__.py b/sf_tool_management/__manifest__.py index 8baa540f..837a8f2e 100644 --- a/sf_tool_management/__manifest__.py +++ b/sf_tool_management/__manifest__.py @@ -38,6 +38,7 @@ 'web.assets_qweb': [ ], 'web.assets_backend': [ + 'sf_tool_management/static/src/change.scss' ] }, diff --git a/sf_tool_management/models/base.py b/sf_tool_management/models/base.py index 40100a93..3cbaf974 100644 --- a/sf_tool_management/models/base.py +++ b/sf_tool_management/models/base.py @@ -387,7 +387,7 @@ class FunctionalToolAssembly(models.Model): else: raise ValidationError('刀柄选择错误,请重新确认!!!') else: - location = self.env['sf.shelf.location'].sudo().search([('barcode', '=', barcode)]) + location = self.env['sf.shelf.location'].sudo().search([('barcode', '=', barcode.upper())]) if location: if location == record.integral_freight_barcode_id: tool_assembly_id.integral_verify = True @@ -782,10 +782,11 @@ class FunctionalToolAssembly(models.Model): """根据BOM对刀具物料进行初始配置""" options = bom.get('options') # 配置刀柄信息 - for handle_id in bom.get('handle_ids'): + handle_ids = self._get_old_tool_material_lot(bom.get('handle_ids')) + for handle_id in handle_ids: if handle_id: if not self.handle_product_id: - self.handle_product_id = handle_id.id + self.handle_product_id = handle_id.product_id.id break # 刀柄之外的物料配置 @@ -820,19 +821,20 @@ class FunctionalToolAssembly(models.Model): location_id = self.env['stock.location'].search([('name', '=', '刀具房')]) stock_quant = self.env['stock.quant'].sudo().search( [('location_id', '=', location_id.id), ('product_id', 'in', material_ids.ids), ('quantity', '>', '0')], - order='lot_id', limit=1) + order='lot_id') if stock_quant: - return stock_quant.lot_id + return [quant.lot_id for quant in stock_quant] else: raise ValidationError(f'【{material_ids[0].cutting_tool_material_id.name}】物料库存不足,请先进行盘点或采购') - def _get_shelf_location_lot(self, lot_id): + def _get_shelf_location_lot(self, lot_ids): """根据所给的刀具物料批次号,返回一个刀具物料货位、批次信息""" - location_lots = self.env['sf.shelf.location.lot'].sudo().search([('lot_id', '=', lot_id.id)]) - if not location_lots: - raise ValidationError(f'没有查询到批次为【{lot_id.name}】物料的货位信息!') - else: - return location_lots[0] + for lot_id in lot_ids: + location_lots = self.env['sf.shelf.location.lot'].sudo().search([('lot_id', '=', lot_id.id)]) + if location_lots: + return location_lots[0] + raise ValidationError(f'【{lot_ids[0].product_id.cutting_tool_material_id.name}】物料在货位库存不足,请先进行盘点入库') + def _get_inventory_bom(self, inventory_id): """获取BOM的刀具物料产品信息""" @@ -1240,7 +1242,7 @@ class FunctionalToolDismantle(models.Model): functional_tool_id = fields.Many2one('sf.functional.cutting.tool.entity', '功能刀具', required=True, tracking=True, domain=[('functional_tool_status', '!=', '已拆除'), - ('current_location', '=', '刀具房')]) + ('current_location', 'in', ['刀具房', '线边刀库'])]) @api.onchange('functional_tool_id') def _onchange_functional_tool_id(self): @@ -1437,14 +1439,26 @@ class FunctionalToolDismantle(models.Model): def confirmation_disassembly(self): logging.info('%s刀具确认开始拆解' % self.dismantle_cause) code = self.code + context = self.env.context if self.functional_tool_id.functional_tool_status == '已拆除': raise ValidationError('Rfid为【%s】名称为【%s】的功能刀具已经拆解,请勿重复操作!' % ( self.functional_tool_id.rfid_dismantle, self.name)) # 对拆解的功能刀具进行校验,只有在刀具房的功能刀具才能拆解 elif self.functional_tool_id.functional_tool_status != '报警': - if self.functional_tool_id.tool_room_num == 0: + if self.functional_tool_id.current_location == '机内刀库': raise ValidationError('Rfid为【%s】的功能刀具当前位置为【%s】,不能进行拆解!' % ( self.rfid, self.functional_tool_id.current_location)) + elif not context.get('TRUE_DISASSEMBLE') and self.functional_tool_id.current_location == '线边刀库': + return { + 'type': 'ir.actions.act_window', + 'res_model': 'sf.functional.tool.dismantle.wiard', + 'name': '刀具寿命未到期', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_functional_tool_dismantle_id': self.id, + 'TRUE_DISASSEMBLE': True} + } # 目标重复校验 self.location_duplicate_check() datas = {'scrap': [], 'picking': []} @@ -1521,11 +1535,18 @@ class FunctionalToolDismantle(models.Model): 'functional_tool_name': self.functional_tool_id.name, 'handle_code_id': self.handle_lot_id.id, 'handle_product_id': self.handle_product_id.id, + 'functional_tool_diameter': self.functional_tool_id.functional_tool_diameter, + 'knife_tip_r_angle': self.functional_tool_id.knife_tip_r_angle, + 'tool_loading_length': self.functional_tool_id.tool_loading_length, + 'functional_tool_length': self.functional_tool_id.functional_tool_length, 'loading_task_source': '3', 'use_tool_time': fields.Datetime.now() + timedelta(hours=4), 'reason_for_applying': '刀具寿命到期' }) + # 将新的组装单更新到对应的功能刀具安全库存的组装单列表中 + self.functional_tool_id.safe_inventory_id.sudo().sf_functional_tool_assembly_ids = [(4, assembly_id.id)] + return { 'type': 'ir.actions.act_window', 'res_model': 'sf.functional.tool.assembly', diff --git a/sf_tool_management/models/functional_tool.py b/sf_tool_management/models/functional_tool.py index 63a31b82..498d9436 100644 --- a/sf_tool_management/models/functional_tool.py +++ b/sf_tool_management/models/functional_tool.py @@ -10,6 +10,7 @@ from odoo.exceptions import ValidationError class FunctionalCuttingToolEntity(models.Model): _name = 'sf.functional.cutting.tool.entity' + _inherit = ['mail.thread'] _description = '功能刀具列表' _order = 'functional_tool_status' @@ -41,7 +42,7 @@ class FunctionalCuttingToolEntity(models.Model): max_lifetime_value = fields.Integer(string='最大寿命值(min)', readonly=True) alarm_value = fields.Integer(string='报警值(min)', readonly=True) used_value = fields.Integer(string='已使用值(min)', readonly=True) - functional_tool_status = fields.Selection([('正常', '正常'), ('报警', '报警'), ('已拆除', '已拆除')], + functional_tool_status = fields.Selection([('正常', '正常'), ('报警', '报警'), ('已拆除', '已拆除')], tracking=True, string='状态', store=True, default='正常') current_location_id = fields.Many2one('stock.location', string='当前位置', compute='_compute_current_location_id', store=True) @@ -62,16 +63,27 @@ class FunctionalCuttingToolEntity(models.Model): for item in self: if item: if item.functional_tool_status == '报警': - # 创建报警刀具拆解单 - self.env['sf.functional.tool.dismantle'].sudo().create({ - 'functional_tool_id': item.ids[0], - 'dismantle_cause': '寿命到期报废' - }) - # 创建刀具报警记录 - self.env['sf.functional.tool.warning'].sudo().create({ - 'rfid': item.rfid, - 'functional_tool_id': item.ids[0] - }) + self.create_tool_dismantle() + + def set_functional_tool_status(self): + # self.write({ + # 'functional_tool_status': '报警' + # }) + self.functional_tool_status = '报警' + self.create_tool_dismantle() + + def create_tool_dismantle(self): + for item in self: + # 创建报警刀具拆解单 + self.env['sf.functional.tool.dismantle'].sudo().create({ + 'functional_tool_id': item.ids[0], + 'dismantle_cause': '寿命到期报废' + }) + # 创建刀具报警记录 + self.env['sf.functional.tool.warning'].sudo().create({ + 'rfid': item.rfid, + 'functional_tool_id': item.ids[0] + }) @api.depends('barcode_id.quant_ids', 'barcode_id.quant_ids.location_id', 'functional_tool_status', 'current_shelf_location_id', 'stock_num') @@ -263,7 +275,7 @@ class FunctionalCuttingToolEntity(models.Model): functional_tool_model_ids.append(functional_tool_model.id) return [(6, 0, functional_tool_model_ids)] - dismantle_num = fields.Integer('拆解单数量', compute='_compute_dismantle_num', store=True) + dismantle_num = fields.Integer('拆解单数量', compute='_compute_dismantle_num', tracking=True, store=True) dismantle_ids = fields.One2many('sf.functional.tool.dismantle', 'functional_tool_id', '拆解单') @api.depends('dismantle_ids') diff --git a/sf_tool_management/models/maintenance_equipment.py b/sf_tool_management/models/maintenance_equipment.py index 12c7f8f3..a3375075 100644 --- a/sf_tool_management/models/maintenance_equipment.py +++ b/sf_tool_management/models/maintenance_equipment.py @@ -107,11 +107,17 @@ class SfMaintenanceEquipment(models.Model): if functional_tool_id.current_location != '机内刀库': # 对功能刀具进行移动到生产线 functional_tool_id.tool_inventory_displacement_out() - functional_tool_id.write({ - 'max_lifetime_value': data['MaxLife'], - 'used_value': data['UseLife'], - 'functional_tool_status': tool_install_time.get(data['State']) - }) + data_tool = { + 'max_lifetime_value': data['MaxLife'], + 'used_value': data['UseLife'], + 'functional_tool_status': tool_install_time.get(data['State']) + } + if (functional_tool_id.functional_tool_status != '报警' + and tool_install_time.get(data['State']) == '报警'): + functional_tool_id.write(data_tool) + functional_tool_id.create_tool_dismantle() + else: + functional_tool_id.write(data_tool) else: logging.info('获取的【%s】设备不存在!!!' % data['DeviceId']) else: diff --git a/sf_tool_management/security/ir.model.access.csv b/sf_tool_management/security/ir.model.access.csv index 8c188464..1ad8a439 100644 --- a/sf_tool_management/security/ir.model.access.csv +++ b/sf_tool_management/security/ir.model.access.csv @@ -40,4 +40,7 @@ access_sf_functional_tool_dismantle_group_sf_tool_user,sf.functional.tool.disman access_sf_functional_tool_dismantle_group_plan_dispatch,sf.functional.tool.dismantle_group_plan_dispatch,model_sf_functional_tool_dismantle,sf_base.group_plan_dispatch,1,0,0,0 access_jikimo_bom,jikimo.bom,model_jikimo_bom,base.group_user,1,1,1,1 -access_jikimo_bom_wizard,jikimo.bom.wizard,model_jikimo_bom_wizard,base.group_user,1,1,1,1 \ No newline at end of file +access_jikimo_bom_wizard,jikimo.bom.wizard,model_jikimo_bom_wizard,base.group_user,1,1,1,1 + +access_sf_functional_tool_dismantle_wiard,sf.functional.tool.dismantle.wiard,model_sf_functional_tool_dismantle_wiard,sf_base.group_sf_tool_user,1,1,1,0 +access_sf_functional_tool_dismantle_wiard_group_plan_dispatch,sf.functional.tool.dismantle.wiard,model_sf_functional_tool_dismantle_wiard,sf_base.group_plan_dispatch,1,0,0,0 diff --git a/sf_tool_management/static/images/replaceIcon.png b/sf_tool_management/static/images/replaceIcon.png new file mode 100644 index 00000000..12d2852f Binary files /dev/null and b/sf_tool_management/static/images/replaceIcon.png differ diff --git a/sf_tool_management/static/src/change.scss b/sf_tool_management/static/src/change.scss index 4c857c8c..9cf26b0a 100644 --- a/sf_tool_management/static/src/change.scss +++ b/sf_tool_management/static/src/change.scss @@ -1,17 +1,47 @@ -.modal-content .o_cp_buttons { - display:none -} +// .modal-content .o_cp_buttons { +// display:none +// } -.modal-content .o_control_panel { - display:none -} +// .modal-content .o_control_panel { +// display:none +// } -.modal-content .o_list_button { +// .modal-content .o_list_button { + +// } + +// .o_form_view .o_field_widget .o_list_renderer { +// width: 100%!important; +// margin:0 auto; +// overflow: auto; +// } +.o_field_widget.o_readonly_modifier.o_field_char.text-success[name=handle_freight_rfid], +.o_field_widget.o_readonly_modifier.o_field_many2one.text-success[name=integral_freight_barcode_id] { + a.text-success{ + span { + color: #999; + } + } } - -.o_form_view .o_field_widget .o_list_renderer { - width: 100%!important; - margin:0 auto; - overflow: auto; +.custom_group:has(.text-success){ + position: relative; + &::after{ + content: ''; + display: block; + width: 72px; + height: 72px; + background: url('/sf_tool_management/static/images/replaceIcon.png') no-repeat center center; + background-size: 100%; + position: absolute; + bottom: 20px; + left: 300px; + } } +.o_field_widget.o_readonly_modifier.o_field_char.text-success[name=handle_freight_rfid] { + display: flex; + align-items: center; + > span { + color: #999; + } +} \ No newline at end of file diff --git a/sf_tool_management/views/functional_tool_views.xml b/sf_tool_management/views/functional_tool_views.xml index 2b872053..25b9346a 100644 --- a/sf_tool_management/views/functional_tool_views.xml +++ b/sf_tool_management/views/functional_tool_views.xml @@ -42,6 +42,7 @@
+