diff --git a/sf_machine_connect/models/__init__.py b/sf_machine_connect/models/__init__.py index ef61fd83..01ae60ae 100644 --- a/sf_machine_connect/models/__init__.py +++ b/sf_machine_connect/models/__init__.py @@ -2,3 +2,4 @@ from . import ftp_client from . import ftp_operate from . import py2opcua from . import res_config_setting +from . import mrp_workorder diff --git a/sf_machine_connect/models/mrp_workorder.py b/sf_machine_connect/models/mrp_workorder.py new file mode 100644 index 00000000..21cc96ac --- /dev/null +++ b/sf_machine_connect/models/mrp_workorder.py @@ -0,0 +1,38 @@ +import re + +from odoo import fields, models, api + + +class ResMrpWorkOrder(models.Model): + _inherit = 'mrp.workorder' + + mixed_search_field = fields.Char(string='坯料产品名称/RFID') + + @api.model + def web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False, + lazy=True, expand=False, expand_limit=None, expand_orderby=False): + domain = domain or [] + for index, item in enumerate(domain): + if isinstance(item, list): + if item[0] == 'mixed_search_field': + if self._is_rfid_code(item[2]): + domain[index] = ['rfid_code', item[1], item[2]] + else: + domain[index] = ['product_tmpl_name', item[1], item[2]] + + return super(ResMrpWorkOrder, self).web_read_group(domain, fields, groupby, limit=limit, offset=offset, orderby=orderby, + lazy=lazy, expand=expand, expand_limit=expand_limit, expand_orderby=expand_orderby) + + def _is_rfid_code(self, tag): + """ + 判断是否是rfid_code + """ + # 基于长度判断(假设RFID标签长度为10到16个字符) + if not 10 <= len(tag) <= 16: + return False + + # 基于字符集判断(仅包含数字和字母) + if not re.match("^[0-9]*$", tag): + return False + + return True \ No newline at end of file diff --git a/sf_machine_connect/views/WorkCenterBarcodes.xml b/sf_machine_connect/views/WorkCenterBarcodes.xml index 9e5d3982..97fee70e 100644 --- a/sf_machine_connect/views/WorkCenterBarcodes.xml +++ b/sf_machine_connect/views/WorkCenterBarcodes.xml @@ -26,6 +26,7 @@ + diff --git a/sf_manufacturing/__manifest__.py b/sf_manufacturing/__manifest__.py index 39da4482..9ea3bb60 100644 --- a/sf_manufacturing/__manifest__.py +++ b/sf_manufacturing/__manifest__.py @@ -15,12 +15,14 @@ 'data/stock_data.xml', 'data/empty_racks_data.xml', 'data/panel_data.xml', + 'data/agv_scheduling_data.xml', 'security/group_security.xml', 'security/ir.model.access.csv', 'wizard/workpiece_delivery_views.xml', 'wizard/rework_wizard_views.xml', 'wizard/production_wizard_views.xml', 'views/mrp_views_menus.xml', + 'views/agv_scheduling_views.xml', 'views/stock_lot_views.xml', 'views/mrp_production_addional_change.xml', 'views/mrp_routing_workcenter_view.xml', @@ -30,7 +32,6 @@ 'views/model_type_view.xml', 'views/agv_setting_views.xml', 'views/sf_maintenance_equipment.xml', - ], 'assets': { @@ -40,7 +41,8 @@ 'web.assets_backend': [ 'sf_manufacturing/static/src/xml/kanban_change.xml', 'sf_manufacturing/static/src/js/kanban_change.js', - 'sf_manufacturing/static/src/scss/kanban_change.scss' + 'sf_manufacturing/static/src/scss/kanban_change.scss', + 'sf_manufacturing/static/src/xml/button_show_on_tree.xml', ] }, diff --git a/sf_manufacturing/controllers/controllers.py b/sf_manufacturing/controllers/controllers.py index f3597093..f9836616 100644 --- a/sf_manufacturing/controllers/controllers.py +++ b/sf_manufacturing/controllers/controllers.py @@ -2,6 +2,8 @@ import logging import json from datetime import datetime + +from odoo.addons.sf_manufacturing.models.agv_scheduling import RepeatTaskException from odoo import http from odoo.http import request @@ -448,6 +450,8 @@ class Manufacturing_Connect(http.Controller): if 'DeviceId' in ret: logging.info('DeviceId:%s' % ret['DeviceId']) if 'IsComplete' in ret: + rfid_codes = [] + workorder_ids = [] if ret['IsComplete'] is True or ret['IsComplete'] is False: for i in range(1, 5): logging.info('F-RfidCode:%s' % i) @@ -455,6 +459,7 @@ class Manufacturing_Connect(http.Controller): rfid_code = ret[f'RfidCode{i}'] logging.info('RfidCode:%s' % rfid_code) if rfid_code is not None: + rfid_codes.append(rfid_code) domain = [ ('rfid_code', '=', rfid_code), ('routing_type', '=', 'CNC加工'), ('state', '!=', 'rework') @@ -462,6 +467,7 @@ class Manufacturing_Connect(http.Controller): workorder = request.env['mrp.workorder'].sudo().search(domain, order='id asc') if workorder: for order in workorder: + workorder_ids.append(order.id) if order.production_line_state == '待上产线': logging.info( '工单产线状态:%s' % order.production_line_state) @@ -470,23 +476,45 @@ class Manufacturing_Connect(http.Controller): ('processing_panel', '=', order.processing_panel)]) if panel_workorder: panel_workorder.write({'production_line_state': '已上产线'}) - workpiece_delivery = request.env['sf.workpiece.delivery'].sudo().search( - [ - ('rfid_code', '=', rfid_code), ('type', '=', '上产线'), - ('production_id', '=', order.production_id.id), - ('workorder_id', '=', order.id), - ('workorder_state', '=', 'done')]) - if workpiece_delivery.status == '待下发': - workpiece_delivery.write({'is_manual_work': True}) + # workpiece_delivery = request.env['sf.workpiece.delivery'].sudo().search( + # [ + # ('rfid_code', '=', rfid_code), ('type', '=', '上产线'), + # ('production_id', '=', order.production_id.id), + # ('workorder_id', '=', order.id), + # ('workorder_state', '=', 'done')]) + # if workpiece_delivery.status == '待下发': + # workpiece_delivery.write({'is_manual_work': True}) + # 下发 else: res = {'Succeed': False, 'ErrorCode': 204, 'Error': 'DeviceId为%s没有对应的已配送工件数据' % ret['DeviceId']} + if ret['IsComplete'] is True: + # 将RFID_CODE对应的工件配送单对应的AGV任务调度状态设置为已配送 + # workorder_id = request.env['mrp.workorder'].sudo().search([ + # ('rfid_code', 'in', rfid_codes), + # ('routing_type', '=', '装夹预调'), ('state', '!=', 'rework') + # ]) + # workorder_id.agv_scheduling_ids.finish_scheduling() + # 将工件配送单状态设置为已配送 + request.env['sf.workpiece.delivery'].sudo().search([ + ('rfid_code', 'in', rfid_codes), + ('type', '=', '上产线'), + ('status', '=', '已下发') + ]).write({'state': '已配送'}) + # 向AGV任务调度下发运送空料架任务 + # 获取设备ID对应的接驳站配置 + agv_site = request.env['sf.agv.site'].sudo().search( + [('name', '=', ret['DeviceId'])], limit=1) + workorders = request.env['mrp.workorder'].browse(workorder_ids) + request.env['sf.agv.scheduling'].add_scheduling(agv_site.id, '运送空料架', workorders) else: res = {'Succeed': False, 'ErrorCode': 203, 'Error': '未传IsComplete字段'} else: res = {'Succeed': False, 'ErrorCode': 201, 'Error': '未传DeviceId字段'} + except RepeatTaskException as e: + logging.info('AGVToProduct error:%s' % e) except Exception as e: - res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} + res = {'Succeed': False, 'ErrorCode': 202, 'Error': str(e)} logging.info('AGVToProduct error:%s' % e) return json.JSONEncoder().encode(res) @@ -509,7 +537,8 @@ class Manufacturing_Connect(http.Controller): logging.info('ret:%s' % ret) if 'DeviceId' in ret: logging.info('DeviceId:%s' % ret['DeviceId']) - delivery_Arr = [] + # delivery_Arr = [] + workorder_ids = [] if 'IsComplete' in ret: if ret['IsComplete'] is True or ret['IsComplete'] is False: for i in range(1, 5): @@ -525,6 +554,7 @@ class Manufacturing_Connect(http.Controller): workorder = request.env['mrp.workorder'].sudo().search(domain, order='id asc') if workorder: for order in workorder: + workorder_ids.append(order.id) if order.production_line_state == '已上产线': logging.info( '工单产线状态:%s' % order.production_line_state) @@ -534,33 +564,42 @@ class Manufacturing_Connect(http.Controller): if panel_workorder: panel_workorder.write({'production_line_state': '已下产线'}) workorder.write({'state': 'to be detected'}) - workpiece_delivery = request.env['sf.workpiece.delivery'].sudo().search( - [ - ('rfid_code', '=', rfid_code), ('type', '=', '下产线'), - ('production_id', '=', order.production_id.id), - ('workorder_id', '=', order.id), - ('workorder_state', '=', 'done')]) - if workpiece_delivery: - delivery_Arr.append(workpiece_delivery.id) + # workpiece_delivery = request.env['sf.workpiece.delivery'].sudo().search( + # [ + # ('rfid_code', '=', rfid_code), ('type', '=', '下产线'), + # ('production_id', '=', order.production_id.id), + # ('workorder_id', '=', order.id), + # ('workorder_state', '=', 'done')]) + # if workpiece_delivery: + # delivery_Arr.append(workpiece_delivery.id) else: res = {'Succeed': False, 'ErrorCode': 204, 'Error': 'DeviceId为%s没有对应的已配送工件数据' % ret['DeviceId']} - if delivery_Arr: - logging.info('delivery_Arr:%s' % delivery_Arr) - delivery_workpiece = request.env['sf.workpiece.delivery'].sudo().search( - [('id', 'in', delivery_Arr)]) - if delivery_workpiece: - logging.info('开始向agv下发下产线任务') - agv_site = request.env['sf.agv.site'].sudo().search([]) - if agv_site: - has_site = agv_site.update_site_state() - if has_site is True: - is_free = delivery_workpiece._check_avgsite_state() - if is_free is True: - delivery_workpiece._delivery_avg() - logging.info('agv下发下产线任务下发完成') + # if delivery_Arr: + # logging.info('delivery_Arr:%s' % delivery_Arr) + # delivery_workpiece = request.env['sf.workpiece.delivery'].sudo().search( + # [('id', 'in', delivery_Arr)]) + # if delivery_workpiece: + # logging.info('开始向agv下发下产线任务') + # agv_site = request.env['sf.agv.site'].sudo().search([]) + # if agv_site: + # has_site = agv_site.update_site_state() + # if has_site is True: + # is_free = delivery_workpiece._check_avgsite_state() + # if is_free is True: + # delivery_workpiece._delivery_avg() + # logging.info('agv下发下产线任务下发完成') + if ret['IsComplete'] is True: + # 向AGV任务调度下发下产线任务 + # 获取设备ID对应的接驳站配置 + agv_site = request.env['sf.agv.site'].sudo().search( + [('name', '=', ret['DeviceId'])], limit=1) + workorders = request.env['mrp.workorder'].browse(workorder_ids) + request.env['sf.agv.scheduling'].add_scheduling(agv_site.id, '下产线', workorders) else: res = {'Succeed': False, 'ErrorCode': 203, 'Error': '未传IsComplete字段'} + except RepeatTaskException as e: + logging.info('AGVToProduct error:%s' % e) except Exception as e: res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} logging.info('AGVDownProduct error:%s' % e) @@ -600,3 +639,27 @@ class Manufacturing_Connect(http.Controller): res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} logging.info('AGVDownProduct error:%s' % e) return json.JSONEncoder().encode(res) + + @http.route('/AutoDeviceApi/GetAgvStationState', type='json', auth='none', methods=['GET', 'POST'], csrf=False, + cors="*") + def AGVStationState(self, **kw): + """ + 中控推送接驳站状态 + :param kw: + :return: + """ + logging.info('AGVStationState:%s' % kw) + try: + res = {'Succeed': True} + datas = request.httprequest.data + ret = json.loads(datas) + request.env['center_control.interface.log'].sudo().create( + {'content': ret, 'name': 'AutoDeviceApi/AGVStationState'}) + logging.info('ret:%s' % ret) + if 'DeviceId' in ret and 'AtHome' in ret: + logging.info('DeviceId:%s, AtHome:%s' % (ret['DeviceId'], ret['AtHome'])) + request.env['sf.agv.site'].update_site_state({ret['DeviceId']: '占用' if ret['AtHome'] else '空闲'}) + except Exception as e: + res = {'Succeed': False, 'ErrorCode': 202, 'Error': str(e)} + logging.info('AGVDownProduct error:%s' % e) + return json.JSONEncoder().encode(res) \ No newline at end of file diff --git a/sf_manufacturing/controllers/workpiece.py b/sf_manufacturing/controllers/workpiece.py index 5c5d6f22..5d79a1b3 100644 --- a/sf_manufacturing/controllers/workpiece.py +++ b/sf_manufacturing/controllers/workpiece.py @@ -25,15 +25,14 @@ class Workpiece(http.Controller): if 'reqCode' in ret: if 'method' in ret: if ret['method'] == 'end': - req_codes = ret['reqCode'].split(',') - for req_code in req_codes: - workpiece_delivery = request.env['sf.workpiece.delivery'].sudo().search( - [('name', '=', req_code.strip()), ('task_completion_time', '=', False)]) - if workpiece_delivery: - workpiece_delivery.write({'status': '已配送', 'task_completion_time': datetime.now()}) - else: - res = {'Succeed': False, 'ErrorCode': 203, - 'Error': '该reqCode暂未查到对应的工件配送记录'} + # 找到对应的AGV调度任务 + agv_scheduling = request.env['sf.agv.scheduling'].sudo().search( + [('name', '=', ret['reqCode']), ('state', '=', '配送中')]) + if agv_scheduling: + agv_scheduling.finish_scheduling() + else: + res = {'Succeed': False, 'ErrorCode': 203, + 'Error': '该reqCode暂未查到对应的AGV任务记录'} else: res = {'Succeed': False, 'ErrorCode': 204, 'Error': '未传method字段'} else: diff --git a/sf_manufacturing/data/agv_scheduling_data.xml b/sf_manufacturing/data/agv_scheduling_data.xml new file mode 100644 index 00000000..c5f6d6a1 --- /dev/null +++ b/sf_manufacturing/data/agv_scheduling_data.xml @@ -0,0 +1,12 @@ + + + + + AGV调度 + sf.agv.scheduling + B%(year)s%(month)s%(day)s + 4 + + + + \ No newline at end of file diff --git a/sf_manufacturing/models/__init__.py b/sf_manufacturing/models/__init__.py index 3de23ef3..b93d613e 100644 --- a/sf_manufacturing/models/__init__.py +++ b/sf_manufacturing/models/__init__.py @@ -9,3 +9,4 @@ from . import stock from . import res_user from . import production_line_base from . import agv_setting +from . import agv_scheduling diff --git a/sf_manufacturing/models/agv_scheduling.py b/sf_manufacturing/models/agv_scheduling.py new file mode 100644 index 00000000..f634154b --- /dev/null +++ b/sf_manufacturing/models/agv_scheduling.py @@ -0,0 +1,232 @@ +import requests + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +import logging + +_logger = logging.getLogger(__name__) + + +class RepeatTaskException(UserError): + pass + + +class AgvScheduling(models.Model): + _name = 'sf.agv.scheduling' + _description = 'agv调度' + _order = 'id desc' + + name = fields.Char('任务单号', index=True, copy=False) + + def _get_agv_route_type_selection(self): + return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection'] + + agv_route_type = fields.Selection(selection=_get_agv_route_type_selection, string='任务类型', required=True) + agv_route_name = fields.Char('任务路线名称') + start_site_id = fields.Many2one('sf.agv.site', '起点接驳站', required=True) + end_site_id = fields.Many2one('sf.agv.site', '终点接驳站', tracking=True) + site_state = fields.Selection([ + ('占用', '占用'), + ('空闲', '空闲')], string='终点接驳站状态', default='占用') + state = fields.Selection([ + ('待下发', '待下发'), + ('配送中', '配送中'), + ('已配送', '已配送'), + ('已取消', '已取消')], string='状态', default='待下发', tracking=True) + workorder_ids = fields.Many2many('mrp.workorder', 'sf_agv_scheduling_mrp_workorder_ref', string='关联工单') + task_create_time = fields.Datetime('任务创建时间') + task_delivery_time = fields.Datetime('任务下发时间') + task_completion_time = fields.Datetime('任务完成时间') + task_duration = fields.Char('任务时长', compute='_compute_task_duration') + + @api.depends('agv_route_type') + def _compute_delivery_workpieces(self): + for record in self: + if record.agv_route_type == '运送空料架': + record.delivery_workpieces = '/' + else: + record.delivery_workpieces = '、'.join(record.workorder_ids.mapped('production_id.name')) + + delivery_workpieces = fields.Char('配送工件', compute=_compute_delivery_workpieces) + + @api.depends('task_completion_time', 'task_delivery_time') + def _compute_task_duration(self): + for rec in self: + if rec.task_completion_time and rec.task_delivery_time: + rec.task_duration = str(rec.task_completion_time - rec.task_delivery_time) + else: + rec.task_duration = '' + + @api.model_create_multi + def create(self, vals_list): + # We generate a standard reference + for vals in vals_list: + vals['name'] = self.env['ir.sequence'].next_by_code('sf.agv.scheduling') or _('New') + return super().create(vals_list) + + def add_scheduling(self, agv_start_site_id, agv_route_type, workorders): + """ add_scheduling(agv_start_site_id, agv_route_type, workorders) -> agv_scheduling + 新增AGV调度 + params: + agv_start_site_id: AGV起点接驳站ID + agv_route_type: AGV任务类型 + workorders: 工单 + """ + if not workorders: + raise UserError(_('工单不能为空')) + # 如果存在相同任务类型工单的AGV调度任务,则提示错误 + agv_scheduling = self.sudo().search([ + ('workorder_ids', 'in', workorders.ids), + ('agv_route_type', '=', agv_route_type), + ('state', 'in', ['待下发', '配送中']) + ], limit=1) + if agv_scheduling: + # 计算agv_scheduling.workorder_ids与workorders的交集 + repetitive_workorders = agv_scheduling.workorder_ids & workorders + raise RepeatTaskException( + '制造订单号【%s】已存在于【%s】AGV调度任务,请勿重复下发!' % + (','.join(repetitive_workorders.mapped('production_id.name')), agv_scheduling.name) + ) + + 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) + ]) + 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_name': idle_route.name}) + else: + # 判断终点接驳站是否为空闲 + idle_routes = agv_routes.filtered(lambda r: r.end_site_id.state == '空闲') + if idle_routes: + # 将空闲的路线按照终点接驳站名称排序 + 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_name': idle_route.name}) + try: + scheduling = self.env['sf.agv.scheduling'].sudo().create(vals) + # 触发空闲接驳站状态更新,触发新任务下发 + if idle_route and idle_route.end_site_id.state == '空闲': + scheduling.dispatch_scheduling(idle_route) + + 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): + """ + 响应AGV接驳站站点状态变化 + params: + agv_site_id: 接驳站ID + agv_site_state: 站点状态('空闲', '占用') + """ + if agv_site_state == '空闲': + # 查询终点接驳站为agv_site_id的AGV路线 + task_routes = self.env['sf.agv.task.route'].sudo().search([('end_site_id', '=', agv_site_id)]) + agv_scheduling = self.env['sf.agv.scheduling'].sudo().search( + [('state', '=', '待下发'), ('agv_route_type', 'in', task_routes.mapped('route_type'))], + order='id asc', + limit=1 + ) + task_route = task_routes.filtered( + lambda r: r.start_site_id == agv_scheduling.start_site_id and r.start_site_id == agv_scheduling.start_site_id + ) + # 下发AGV调度任务并修改接驳站状态为占用 + agv_scheduling.dispatch_scheduling(task_route) + + def _delivery_avg(self): + config = self.env['res.config.settings'].get_values() + position_code_arr = [{ + 'positionCode': self.start_site_id.name, + 'code': '00' + }, { + 'positionCode': self.end_site_id.name, + 'code': '00' + }] + res = {'reqCode': self.name, 'reqTime': '', 'clientCode': '', 'tokenCode': '', + 'taskTyp': 'F01', 'ctnrTyp': '', 'ctnrCode': '', 'wbCode': config['wbcode'], + 'positionCodePath': position_code_arr, + 'podCode': '', + 'podDir': '', 'materialLot': '', 'priority': '', 'taskCode': '', 'agvCode': '', 'materialLot': '', + 'data': ''} + try: + logging.info('AGV请求路径:%s' % config['agv_rcs_url']) + logging.info('AGV-json:%s' % res) + headers = {'Content-Type': 'application/json'} + ret = requests.post((config['agv_rcs_url']), json=res, headers=headers) + ret = ret.json() + logging.info('config-ret:%s' % ret) + if ret['code'] == 0: + return True + else: + raise UserError(ret['message']) + except Exception as e: + logging.info('config-e:%s' % e) + raise UserError("工件配送请求agv失败:%s" % e) + + def button_cancel(self): + # 弹出二次确认窗口后执行 + for rec in self: + if rec.state != '待下发': + raise UserError('只有待下发状态的AGV调度任务才能取消!') + rec.state = '已取消' + + def finish_scheduling(self): + """ + 完成调度任务 + """ + for rec in self: + if rec.state != '配送中': + return False + _logger.info('AGV任务调度:完成任务%s' % rec) + rec.state = '已配送' + rec.task_completion_time = fields.Datetime.now() + + def dispatch_scheduling(self, agv_task_route): + """ + 下发调度任务 + params: + agv_route sf.agv.task.route对象 + """ + for rec in self: + if rec.state != '待下发': + return False + _logger.info('AGV任务调度:下发调度任务,路线为%s' % agv_task_route) + # rec._delivery_avg() + rec.state = '配送中' + rec.task_delivery_time = fields.Datetime.now() + rec.site_state = '空闲' + rec.end_site_id = agv_task_route.end_site_id.id + rec.agv_route_name = agv_task_route.name + # 更新接驳站状态 + rec.env['sf.agv.site'].update_site_state({rec.end_site_id.name: '占用'}, False) + + def write(self, vals): + if vals.get('state', False): + if vals['state'] == '已取消': + self.env['sf.workpiece.delivery'].search([('agv_scheduling_id', '=', self.id)]).write({'status': '待下发'}) + elif vals['state'] == '已配送': + self.env['sf.workpiece.delivery'].search([('agv_scheduling_id', '=', self.id)]).write({'status': '已配送'}) + return super().write(vals) + + +class ResMrpWorkOrder(models.Model): + _inherit = 'mrp.workorder' + + agv_scheduling_ids = fields.Many2many( + 'sf.agv.scheduling', + 'sf_agv_scheduling_mrp_workorder_ref', + string='AGV调度', + domain=[('state', '!=', '已取消')]) diff --git a/sf_manufacturing/models/agv_setting.py b/sf_manufacturing/models/agv_setting.py index 06f9edba..820ed539 100644 --- a/sf_manufacturing/models/agv_setting.py +++ b/sf_manufacturing/models/agv_setting.py @@ -5,50 +5,77 @@ import time from odoo import fields, models, api from odoo.exceptions import UserError +_logger = logging.getLogger(__name__) + class AgvSetting(models.Model): _name = 'sf.agv.site' _description = 'agv站点' name = fields.Char('位置编号') - owning_region = fields.Char('所属区域') + # owning_region = fields.Char('所属区域') state = fields.Selection([ ('占用', '占用'), ('空闲', '空闲')], string='状态') divide_the_work = fields.Char('主要分工') active = fields.Boolean('有效', default=True) + workcenter_id = fields.Many2one(string='所属区域', comodel_name='mrp.workcenter', tracking=True, + domain=[('is_agv_scheduling', '=', True)]) - def update_site_state(self): - # 调取中控的接驳站接口并修改对应站点的状态 - config = self.env['res.config.settings'].get_values() - # token = sf_sync_config['token'Ba F2CF5DCC-1A00-4234-9E95-65603F70CC8A] - headers = {'Authorization': config['center_control_Authorization']} - center_control_url = config['center_control_url'] + "/AutoDeviceApi/GetAgvStationState?date=" - timestamp = int(time.time()) - center_control_url += str(timestamp) - logging.info('工件配送-请求中控地址:%s' % center_control_url) - try: - center_control_r = requests.get(center_control_url, headers=headers, timeout=10) # 设置超时为60秒 - ret = center_control_r.json() - logging.info('工件配送-请求中控站点信息:%s' % ret) - self.env['center_control.interface.log'].sudo().create( - {'content': ret, 'name': 'AutoDeviceApi/GetAgvStationState?date=%s' % str(timestamp)}) - if ret['Succeed'] is True: - datas = ret['Datas'] - for item in self: - for da in datas: - if da['DeviceId'] == item.name: - if da['AtHome'] is True: - item.state = '占用' - else: - item.state = '空闲' - return True - except requests.exceptions.Timeout: - logging.error('工件配送-请求中控接口超时') - return False - except requests.exceptions.RequestException as e: - logging.error('工件配送-请求中控接口错误: %s', e) - return False + # name必须唯一 + _sql_constraints = [ + ('name_uniq', 'unique (name)', '站点编号必须唯一!'), + ] + + # def update_site_state(self): + # # 调取中控的接驳站接口并修改对应站点的状态 + # config = self.env['res.config.settings'].get_values() + # # token = sf_sync_config['token'Ba F2CF5DCC-1A00-4234-9E95-65603F70CC8A] + # headers = {'Authorization': config['center_control_Authorization']} + # center_control_url = config['center_control_url'] + "/AutoDeviceApi/GetAgvStationState?date=" + # timestamp = int(time.time()) + # center_control_url += str(timestamp) + # logging.info('工件配送-请求中控地址:%s' % center_control_url) + # try: + # center_control_r = requests.get(center_control_url, headers=headers, timeout=10) # 设置超时为60秒 + # ret = center_control_r.json() + # logging.info('工件配送-请求中控站点信息:%s' % ret) + # self.env['center_control.interface.log'].sudo().create( + # {'content': ret, 'name': 'AutoDeviceApi/GetAgvStationState?date=%s' % str(timestamp)}) + # if ret['Succeed'] is True: + # datas = ret['Datas'] + # for item in self: + # for da in datas: + # if da['DeviceId'] == item.name: + # if da['AtHome'] is True: + # item.state = '占用' + # else: + # item.state = '空闲' + # return True + # except requests.exceptions.Timeout: + # logging.error('工件配送-请求中控接口超时') + # return False + # except requests.exceptions.RequestException as e: + # logging.error('工件配送-请求中控接口错误: %s', e) + # return False + + def update_site_state(self, agv_site_state_arr, notify=True): + """ + 更新接驳站状态 + params: + agv_site_state_arr: {'A01': '空闲', 'B01': '占用'} + notify: 是否通知调度(非中控发起的状态改变不触发调度任务) + """ + 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 + if notify: + self.env['sf.agv.scheduling'].on_site_state_change(agv_site.id, agv_site.state) + else: + _logger.error("更新失败:接驳站站点错误!%s" % agv_site_name) + raise UserError("更新失败:接驳站站点错误!") class AgvTaskRoute(models.Model): @@ -71,6 +98,17 @@ class AgvTaskRoute(models.Model): if self.end_site_id == self.start_site_id: raise UserError("您选择的终点接驳站与起点接驳站重复,请重新选择") + workcenter_id = fields.Many2one(string='所属区域', comodel_name='mrp.workcenter', domain=[('is_agv_scheduling', '=', True)], + compute="_compute_region") + + @api.depends('end_site_id') + def _compute_region(self): + for record in self: + if record.end_site_id: + record.workcenter_id = record.end_site_id.workcenter_id + else: + record.workcenter_id = None + class Center_controlInterfaceLog(models.Model): _name = 'center_control.interface.log' diff --git a/sf_manufacturing/models/mrp_workcenter.py b/sf_manufacturing/models/mrp_workcenter.py index 64cf2d8d..461ef6ad 100644 --- a/sf_manufacturing/models/mrp_workcenter.py +++ b/sf_manufacturing/models/mrp_workcenter.py @@ -124,6 +124,8 @@ class ResWorkcenter(models.Model): res[wc_id] = [(datetime.fromtimestamp(s), datetime.fromtimestamp(e)) for s, e, _ in final_intervals_wc] return res + # AGV是否可配送 + is_agv_scheduling = fields.Boolean(string="AGV所属区域", tracking=True) class ResWorkcenterProductivity(models.Model): _inherit = 'mrp.workcenter.productivity' diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index ccdfd8d1..05812f44 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -371,10 +371,10 @@ class ResMrpWorkOrder(models.Model): vals['leave_id'] = leave.id self.write(vals) - @api.onchange('rfid_code') - def _onchange(self): - if self.rfid_code and self.state == 'progress': - self.workpiece_delivery_ids[0].write({'rfid_code': self.rfid_code}) + # @api.onchange('rfid_code') + # def _onchange(self): + # if self.rfid_code and self.state == 'progress': + # self.workpiece_delivery_ids[0].write({'rfid_code': self.rfid_code}) def get_plan_workorder(self, production_line): tomorrow = (date.today() + timedelta(days=+1)).strftime("%Y-%m-%d") @@ -687,25 +687,34 @@ class ResMrpWorkOrder(models.Model): k, item), 'cmm_ids': False if route.routing_type != 'CNC加工' else self.env['sf.cmm.program']._json_cmm_program(k, item), - 'workpiece_delivery_ids': False if not route.routing_type == '装夹预调' else self._json_workpiece_delivery_list( - production) + # 'workpiece_delivery_ids': False if not route.routing_type == '装夹预调' else self._json_workpiece_delivery_list( + # production) }] return workorders_values_str - def _json_workpiece_delivery_list(self, production): - up_route = self.env['sf.agv.task.route'].search([('route_type', '=', '上产线')], limit=1, order='id asc') - down_route = self.env['sf.agv.task.route'].search([('route_type', '=', '下产线')], limit=1, order='id asc') + def _json_workpiece_delivery_list(self): + # 修改在装夹工单完成后,生成上产线的工件配送单 + + # up_route = self.env['sf.agv.task.route'].search([('route_type', '=', '上产线')], limit=1, order='id asc') + # down_route = self.env['sf.agv.task.route'].search([('route_type', '=', '下产线')], limit=1, order='id asc') return [ [0, '', - {'production_id': production.id, 'production_line_id': production.production_line_id.id, 'type': '上产线', - 'route_id': up_route.id, - 'feeder_station_start_id': up_route.start_site_id.id, - 'feeder_station_destination_id': up_route.end_site_id.id}], - [0, '', - {'production_id': production.id, 'production_line_id': production.production_line_id.id, 'type': '下产线', - 'route_id': down_route.id, - 'feeder_station_start_id': down_route.start_site_id.id, - 'feeder_station_destination_id': down_route.end_site_id.id}]] + { + 'production_id': self.production_id.id, + 'production_line_id': self.production_id.production_line_id.id, + 'type': '上产线', + 'is_cnc_program_down': True + # 'route_id': up_route.id, + # 'feeder_station_start_id': agv_start_site_id, + # 'feeder_station_destination_id': up_route.end_site_id.id + } + ], + # [0, '', + # {'production_id': production.id, 'production_line_id': production.production_line_id.id, 'type': '下产线', + # 'route_id': down_route.id, + # 'feeder_station_start_id': down_route.start_site_id.id, + # 'feeder_station_destination_id': down_route.end_site_id.id}] + ] # 拼接工单对象属性值(表面工艺) def _json_workorder_surface_process_str(self, production, route, process_parameter, supplier_id): @@ -1149,6 +1158,8 @@ class ResMrpWorkOrder(models.Model): record.process_state = '待加工' # record.write({'process_state': '待加工'}) record.production_id.process_state = '待加工' + # 生成工件配送单 + record.workpiece_delivery_ids = record._json_workpiece_delivery_list() if record.routing_type == 'CNC加工': record.process_state = '待解除装夹' # record.write({'process_state': '待加工'}) @@ -1236,6 +1247,7 @@ class ResMrpWorkOrder(models.Model): record.production_id.button_mark_done1() # record.production_id.state = 'done' + # 将FTP的检测报告文件下载到临时目录 def download_reportfile_tmp(self, workorder, reportpath): logging.info('reportpath/ftp地址:%s' % reportpath) @@ -1275,6 +1287,66 @@ class ResMrpWorkOrder(models.Model): else: raise UserError("无关联制造订单或关联序列号,无法打印。请检查!") + @api.model + def get_views(self, views, options=None): + res = super().get_views(views, options) + if res['views'].get('list', {}) and self.env.context.get('search_default_workcenter_id'): + workcenter = self.env['mrp.workcenter'].browse(self.env.context.get('search_default_workcenter_id')) + tree_view = res['views']['list'] + if workcenter.name == '工件拆卸中心': + arch = etree.fromstring(tree_view['arch']) + # 查找 tree 标签 + tree_element = arch.xpath("//tree")[0] + + # 查找或创建 header 标签 + header_element = tree_element.find('header') + if header_element is None: + header_element = etree.Element('header') + tree_element.insert(0, header_element) + + # 创建并添加按钮元素 + button_element = etree.Element('button', { + 'name': 'button_delivery', + 'type': 'object', + 'string': '解除装夹', + 'class': 'btn-primary', + # 'className': 'btn-primary', + 'modifiers': '{"force_show": 1}' + }) + header_element.append(button_element) + + # 更新 tree_view 的 arch + tree_view['arch'] = etree.tostring(arch, encoding='unicode') + return res + + def button_delivery(self): + production_ids = [] + workorder_ids = [] + delivery_type = '运送空料架' + max_num = 4 # 最大配送数量 + if len(self) > max_num: + raise UserError('仅限于拆卸1-4个制造订单,请重新选择') + for item in self: + if item.state != 'ready': + raise UserError('请选择状态为【就绪】的工单进行解除装夹') + + production_ids.append(item.production_id.id) + workorder_ids.append(item.id) + return { + 'name': _('确认'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'sf.workpiece.delivery.wizard', + 'target': 'new', + 'context': { + # 'default_delivery_ids': [(6, 0, delivery_ids)], + 'default_production_ids': [(6, 0, production_ids)], + 'default_delivery_type': delivery_type, + 'default_workorder_ids': [(6, 0, workorder_ids)], + 'default_workcenter_id': self.env.context.get('default_workcenter_id'), + 'default_confirm_button': '确认解除' + }} + class CNCprocessing(models.Model): _name = 'sf.cnc.processing' @@ -1554,20 +1626,25 @@ class WorkPieceDelivery(models.Model): feeder_station_destination_id = fields.Many2one('sf.agv.site', '目的接驳站') task_delivery_time = fields.Datetime('任务下发时间') task_completion_time = fields.Datetime('任务完成时间') - type = fields.Selection( - [('上产线', '上产线'), ('下产线', '下产线'), ('运送空料架', '运送空料架')], string='类型') + + def _get_agv_route_type_selection(self): + return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection'] + + type = fields.Selection(selection=_get_agv_route_type_selection, string='类型') delivery_duration = fields.Float('配送时长', compute='_compute_delivery_duration') status = fields.Selection( - [('待下发', '待下发'), ('待配送', '待配送'), ('已配送', '已配送'), ('已取消', '已取消')], string='状态', - default='待下发', - tracking=True) + [('待下发', '待下发'), ('已下发', '已下发'), ('已配送', '已配送'), ('已取消', '已取消')], string='状态', + default='待下发', tracking=True) is_cnc_program_down = fields.Boolean('程序是否下发', default=False, tracking=True) is_manual_work = fields.Boolean('人工操作', default=False) active = fields.Boolean(string="有效", default=True) + agv_scheduling_id = fields.Many2one('sf.agv.scheduling', 'AGV任务调度') + + @api.model def create(self, vals): - if vals['route_id'] and vals.get('type') is None: + if vals.get('route_id') and vals.get('type') is None: vals['type'] = '运送空料架' else: if vals.get('name', '/') == '/' or vals.get('name', '/') is False: @@ -1615,84 +1692,45 @@ class WorkPieceDelivery(models.Model): def button_delivery(self): delivery_ids = [] production_ids = [] + workorder_ids = [] is_cnc_down = 0 is_not_production_line = 0 - is_not_route = 0 same_production_line_id = None - same_route_id = None - down_status = '待下发' - production_type = None - num = 0 + delivery_type = '上产线' + max_num = 4 # 最大配送数量 + if len(self) > max_num: + raise UserError('仅限于配送1-4个制造订单,请重新选择') for item in self: - num += 1 - if production_type is None: - production_type = item.type - if item.type == "运送空料架": - if num >= 2: - raise UserError('仅选择一条路线进行配送,请重新选择') - else: - delivery_ids.append(item.id) - else: - if num > 4: - raise UserError('仅限于配送1-4个制造订单,请重新选择') - if item.status in ['待配送', '已配送']: - raise UserError('请选择状态为【待下发】的制造订单进行配送') - if item.route_id: - if same_route_id is None: - same_route_id = item.route_id.id - if item.route_id.id != same_route_id: - is_not_route += 1 - # else: - # raise UserError('请选择【任务路线】再进行配送') - # if item.production_id.production_line_state == '已下产线' and item.state == '待下发' and item.type == '下产线': - # raise UserError('该制造订单已下产线,无需配送') - if production_type != item.type: - raise UserError('请选择类型为%s的制造订单进行配送' % production_type) - if down_status != item.status: - up_workpiece = self.search([('type', '=', '上产线'), ('production_id', '=', item.production_id), - ('status', '=', '待下发')]) - if up_workpiece: - raise UserError('您所选择的制造订单暂未上产线,请在上产线后再进行配送') - else: - raise UserError('请选择状态为【待下发】的制造订单进行配送') - - if same_production_line_id is None: - same_production_line_id = item.production_line_id.id - if item.production_line_id.id != same_production_line_id: - is_not_production_line += 1 - if item.is_cnc_program_down is False: - is_cnc_down += 1 - if is_cnc_down == 0 and is_not_production_line == 0 and is_not_route == 0: - delivery_ids.append(item.id) - production_ids.append(item.production_id.id) + if item.status != '待下发': + raise UserError('请选择状态为【待下发】的制造订单进行配送') + if same_production_line_id is None: + same_production_line_id = item.production_line_id.id + if item.production_line_id.id != same_production_line_id: + is_not_production_line += 1 + if item.is_cnc_program_down is False: + is_cnc_down += 1 + if is_cnc_down == 0 and is_not_production_line == 0: + delivery_ids.append(item.id) + production_ids.append(item.production_id.id) + workorder_ids.append(item.workorder_id.id) if is_cnc_down >= 1: raise UserError('您所选择制造订单的【CNC程序】暂未下发,请在程序下发后再进行配送') if is_not_production_line >= 1: raise UserError('您所选择制造订单的【目的生产线】不一致,请重新确认') - if is_not_route >= 1: - raise UserError('您所选择制造订单的【任务路线】不一致,请重新确认') - is_free = self._check_avgsite_state() - if is_free is True: - if delivery_ids: - return { - 'name': _('确认'), - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'res_model': 'sf.workpiece.delivery.wizard', - 'target': 'new', - 'context': { - 'default_delivery_ids': [(6, 0, delivery_ids)], - 'default_production_ids': [(6, 0, production_ids)], - 'default_destination_production_line_id': same_production_line_id, - 'default_route_id': same_route_id, - 'default_type': production_type, - }} - else: - if production_type == '运送空料架': - raise UserError("您所选择的【任务路线】的【终点接驳站】已占用,请在该接驳站空闲时进行配送") - else: - raise UserError( - "您所选择制造订单的【任务路线】的【终点接驳站】已占用,请在该接驳站空闲时或选择其他路线进行配送") + return { + 'name': _('确认'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'sf.workpiece.delivery.wizard', + 'target': 'new', + 'context': { + 'default_delivery_ids': [(6, 0, delivery_ids)], + 'default_production_ids': [(6, 0, production_ids)], + 'default_delivery_type': delivery_type, + 'default_workorder_ids': [(6, 0, workorder_ids)], + 'default_confirm_button': '确认配送' + }} + # 验证agv站点是否可用 def _check_avgsite_state(self): @@ -1798,6 +1836,7 @@ class WorkPieceDelivery(models.Model): obj.delivery_duration = 0.0 + class CMMprogram(models.Model): _name = 'sf.cmm.program' _description = "CMM程序" diff --git a/sf_manufacturing/security/ir.model.access.csv b/sf_manufacturing/security/ir.model.access.csv index 9b5b5e28..f8affb19 100644 --- a/sf_manufacturing/security/ir.model.access.csv +++ b/sf_manufacturing/security/ir.model.access.csv @@ -150,5 +150,7 @@ access_sf_processing_panel_group_sf_order_user,sf_processing_panel_group_sf_orde access_sf_production_wizard_group_sf_order_user,sf_production_wizard_group_sf_order_user,model_sf_production_wizard,sf_base.group_sf_order_user,1,1,1,0 access_sf_processing_panel_group_plan_dispatch,sf_processing_panel_group_plan_dispatch,model_sf_processing_panel,sf_base.group_plan_dispatch,1,1,1,0 +access_sf_agv_scheduling_admin,sf_agv_scheduling_admin,model_sf_agv_scheduling,base.group_system,1,1,1,1 + diff --git a/sf_manufacturing/static/src/xml/button_show_on_tree.xml b/sf_manufacturing/static/src/xml/button_show_on_tree.xml new file mode 100644 index 00000000..00533841 --- /dev/null +++ b/sf_manufacturing/static/src/xml/button_show_on_tree.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sf_manufacturing/views/agv_scheduling_views.xml b/sf_manufacturing/views/agv_scheduling_views.xml new file mode 100644 index 00000000..642e65cd --- /dev/null +++ b/sf_manufacturing/views/agv_scheduling_views.xml @@ -0,0 +1,54 @@ + + + + + + agv调度 + sf.agv.scheduling + + + + + + + + + + + + + + + +