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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AGV调度
+ sf.agv.scheduling
+ tree
+
+
+
+
+
\ No newline at end of file
diff --git a/sf_manufacturing/views/agv_setting_views.xml b/sf_manufacturing/views/agv_setting_views.xml
index 377bee74..62e3c623 100644
--- a/sf_manufacturing/views/agv_setting_views.xml
+++ b/sf_manufacturing/views/agv_setting_views.xml
@@ -8,7 +8,7 @@
-
+
@@ -40,8 +40,9 @@
-
+
+
+
diff --git a/sf_manufacturing/views/mrp_workcenter_views.xml b/sf_manufacturing/views/mrp_workcenter_views.xml
index ad433bf6..d0b4bee6 100644
--- a/sf_manufacturing/views/mrp_workcenter_views.xml
+++ b/sf_manufacturing/views/mrp_workcenter_views.xml
@@ -182,6 +182,7 @@
+
diff --git a/sf_manufacturing/views/mrp_workorder_view.xml b/sf_manufacturing/views/mrp_workorder_view.xml
index 0c4fcd38..b0815ed5 100644
--- a/sf_manufacturing/views/mrp_workorder_view.xml
+++ b/sf_manufacturing/views/mrp_workorder_view.xml
@@ -40,10 +40,15 @@
+
+
+
+
+
{'invisible': ['|', '|', '|','|','|', ('production_state','in', ('draft',
'done',
'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel')),
- ('is_user_working', '!=', False),("user_permissions","=",False),("name","=","CNC加工")]}
+ ('is_user_working', '!=', False),("user_permissions","=",False),("name","in",("CNC加工","解除装夹"))]}
@@ -153,8 +158,10 @@
+
+
+ attrs="{'invisible': ['|', '|', '|', '|', ('routing_type', '=', '解除装夹'), ('production_state','in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel','to be detected')), ('is_user_working', '!=', False)]}"/>