1、优化功能刀具安全库存数量计算方法;2、货位看板模型添加功能刀具Rfid、名称字段
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
'views/tool_material_search.xml',
|
'views/tool_material_search.xml',
|
||||||
'views/fixture_material_search_views.xml',
|
'views/fixture_material_search_views.xml',
|
||||||
'views/menu_view.xml',
|
'views/menu_view.xml',
|
||||||
|
'views/stock.xml',
|
||||||
'data/tool_data.xml',
|
'data/tool_data.xml',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ from . import functional_tool_enroll
|
|||||||
from . import fixture_material_search
|
from . import fixture_material_search
|
||||||
from . import fixture_enroll
|
from . import fixture_enroll
|
||||||
from . import temporary_data_processing_methods
|
from . import temporary_data_processing_methods
|
||||||
|
from . import stock
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ class FunctionalCuttingToolEntity(models.Model):
|
|||||||
handle_length = fields.Float(string='刀柄长度(mm)', readonly=True, digits=(10, 3))
|
handle_length = fields.Float(string='刀柄长度(mm)', readonly=True, digits=(10, 3))
|
||||||
functional_tool_length = fields.Float(string='伸出长(mm)', readonly=True, digits=(10, 3))
|
functional_tool_length = fields.Float(string='伸出长(mm)', readonly=True, digits=(10, 3))
|
||||||
effective_length = fields.Float(string='有效长(mm)', readonly=True)
|
effective_length = fields.Float(string='有效长(mm)', readonly=True)
|
||||||
tool_room_num = fields.Integer(string='刀具房数量', readonly=True)
|
tool_room_num = fields.Integer(string='刀具房数量', compute='_compute_num', store=True)
|
||||||
line_edge_knife_library_num = fields.Integer(string='线边刀库数量', readonly=True)
|
line_edge_knife_library_num = fields.Integer(string='线边刀库数量', compute='_compute_num', store=True)
|
||||||
machine_knife_library_num = fields.Integer(string='机内刀库数量', readonly=True)
|
machine_knife_library_num = fields.Integer(string='机内刀库数量', compute='_compute_num', store=True)
|
||||||
max_lifetime_value = fields.Integer(string='最大寿命值(min)', readonly=True)
|
max_lifetime_value = fields.Integer(string='最大寿命值(min)', readonly=True)
|
||||||
alarm_value = fields.Integer(string='报警值(min)', readonly=True)
|
alarm_value = fields.Integer(string='报警值(min)', readonly=True)
|
||||||
used_value = fields.Integer(string='已使用值(min)', readonly=True)
|
used_value = fields.Integer(string='已使用值(min)', readonly=True)
|
||||||
@@ -57,44 +57,46 @@ class FunctionalCuttingToolEntity(models.Model):
|
|||||||
@api.depends('barcode_id.quant_ids', 'functional_tool_status')
|
@api.depends('barcode_id.quant_ids', 'functional_tool_status')
|
||||||
def _compute_current_location_id(self):
|
def _compute_current_location_id(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
if record.barcode_id.quant_ids:
|
|
||||||
for quant_id in record.barcode_id.quant_ids:
|
|
||||||
if quant_id.inventory_quantity_auto_apply > 0:
|
|
||||||
record.current_location_id = quant_id.location_id
|
|
||||||
if quant_id.location_id.name == '制造前':
|
|
||||||
if not record.current_shelf_location_id:
|
|
||||||
record.current_location = '机内刀库'
|
|
||||||
else:
|
|
||||||
record.current_location = '线边刀库'
|
|
||||||
else:
|
|
||||||
record.current_location = '刀具房'
|
|
||||||
if record.current_location_id:
|
|
||||||
record.sudo().get_location_num()
|
|
||||||
else:
|
|
||||||
record.current_location_id = False
|
|
||||||
record.current_location = False
|
|
||||||
if record.functional_tool_status == '已拆除':
|
if record.functional_tool_status == '已拆除':
|
||||||
record.current_location_id = False
|
record.current_location_id = False
|
||||||
record.current_location = False
|
record.current_location = False
|
||||||
record.tool_room_num = 0
|
else:
|
||||||
record.line_edge_knife_library_num = 0
|
if record.barcode_id.quant_ids:
|
||||||
record.machine_knife_library_num = 0
|
for quant_id in record.barcode_id.quant_ids:
|
||||||
|
if quant_id.inventory_quantity_auto_apply > 0:
|
||||||
|
record.current_location_id = quant_id.location_id
|
||||||
|
if quant_id.location_id.name == '制造前':
|
||||||
|
if not record.current_shelf_location_id:
|
||||||
|
record.current_location = '机内刀库'
|
||||||
|
else:
|
||||||
|
record.current_location = '线边刀库'
|
||||||
|
else:
|
||||||
|
record.current_location = '刀具房'
|
||||||
|
else:
|
||||||
|
record.current_location_id = False
|
||||||
|
record.current_location = False
|
||||||
|
|
||||||
def get_location_num(self):
|
@api.depends('current_location', 'functional_tool_status')
|
||||||
|
def _compute_num(self):
|
||||||
"""
|
"""
|
||||||
计算库存位置数量
|
计算库存位置数量
|
||||||
"""
|
"""
|
||||||
for obj in self:
|
for obj in self:
|
||||||
if obj.current_location_id:
|
if obj.functional_tool_status == '已拆除':
|
||||||
obj.tool_room_num = 0
|
obj.tool_room_num = 0
|
||||||
obj.line_edge_knife_library_num = 0
|
obj.line_edge_knife_library_num = 0
|
||||||
obj.machine_knife_library_num = 0
|
obj.machine_knife_library_num = 0
|
||||||
if obj.current_location in ['刀具房']:
|
else:
|
||||||
obj.tool_room_num = 1
|
if obj.current_location_id:
|
||||||
elif "线边刀库" in obj.current_location:
|
obj.tool_room_num = 0
|
||||||
obj.line_edge_knife_library_num = 1
|
obj.line_edge_knife_library_num = 0
|
||||||
elif "机内刀库" in obj.current_location:
|
obj.machine_knife_library_num = 0
|
||||||
obj.machine_knife_library_num = 1
|
if obj.current_location in ['刀具房']:
|
||||||
|
obj.tool_room_num = 1
|
||||||
|
elif "线边刀库" in obj.current_location:
|
||||||
|
obj.line_edge_knife_library_num = 1
|
||||||
|
elif "机内刀库" in obj.current_location:
|
||||||
|
obj.machine_knife_library_num = 1
|
||||||
|
|
||||||
def tool_in_out_stock_location(self, location_id):
|
def tool_in_out_stock_location(self, location_id):
|
||||||
tool_room_id = self.env['stock.location'].search([('name', '=', '刀具房')])
|
tool_room_id = self.env['stock.location'].search([('name', '=', '刀具房')])
|
||||||
@@ -398,10 +400,10 @@ class RealTimeDistributionOfFunctionalTools(models.Model):
|
|||||||
group_expand='_read_mrs_cutting_tool_type_ids', store=True)
|
group_expand='_read_mrs_cutting_tool_type_ids', store=True)
|
||||||
diameter = fields.Float(string='刀具直径(mm)', readonly=False)
|
diameter = fields.Float(string='刀具直径(mm)', readonly=False)
|
||||||
knife_tip_r_angle = fields.Float(string='刀尖R角(mm)', readonly=False)
|
knife_tip_r_angle = fields.Float(string='刀尖R角(mm)', readonly=False)
|
||||||
tool_stock_num = fields.Integer(string='刀具房数量')
|
tool_stock_num = fields.Integer(string='刀具房数量', compute='_compute_stock_num', store=True)
|
||||||
side_shelf_num = fields.Integer(string='线边刀库数量')
|
side_shelf_num = fields.Integer(string='线边刀库数量', compute='_compute_stock_num', store=True)
|
||||||
on_tool_stock_num = fields.Integer(string='机内刀库数量')
|
on_tool_stock_num = fields.Integer(string='机内刀库数量', compute='_compute_stock_num', store=True)
|
||||||
tool_stock_total = fields.Integer(string='当前库存量', readonly=True)
|
tool_stock_total = fields.Integer(string='当前库存量', compute='_compute_tool_stock_total', store=True)
|
||||||
min_stock_num = fields.Integer('最低库存量')
|
min_stock_num = fields.Integer('最低库存量')
|
||||||
max_stock_num = fields.Integer('最高库存量')
|
max_stock_num = fields.Integer('最高库存量')
|
||||||
batch_replenishment_num = fields.Integer('批次补货量', readonly=True, compute='_compute_batch_replenishment_num',
|
batch_replenishment_num = fields.Integer('批次补货量', readonly=True, compute='_compute_batch_replenishment_num',
|
||||||
@@ -474,10 +476,6 @@ class RealTimeDistributionOfFunctionalTools(models.Model):
|
|||||||
def _compute_batch_replenishment_num(self):
|
def _compute_batch_replenishment_num(self):
|
||||||
for tool in self:
|
for tool in self:
|
||||||
if tool:
|
if tool:
|
||||||
# 计算刀具房数量、线边刀库数量、机内刀库数量
|
|
||||||
tool.sudo().get_stock_num(tool)
|
|
||||||
# 计算当前库存量
|
|
||||||
tool.sudo().tool_stock_total = tool.tool_stock_num + tool.side_shelf_num + tool.on_tool_stock_num
|
|
||||||
# 如果当前库存量小于最低库存量,计算批次补货量
|
# 如果当前库存量小于最低库存量,计算批次补货量
|
||||||
tool.sudo().open_batch_replenishment_num(tool)
|
tool.sudo().open_batch_replenishment_num(tool)
|
||||||
|
|
||||||
@@ -496,6 +494,38 @@ class RealTimeDistributionOfFunctionalTools(models.Model):
|
|||||||
else:
|
else:
|
||||||
tool.sudo().batch_replenishment_num = 0
|
tool.sudo().batch_replenishment_num = 0
|
||||||
|
|
||||||
|
@api.depends('sf_functional_tool_entity_ids.tool_room_num',
|
||||||
|
'sf_functional_tool_entity_ids.line_edge_knife_library_num',
|
||||||
|
'sf_functional_tool_entity_ids.machine_knife_library_num')
|
||||||
|
def _compute_stock_num(self):
|
||||||
|
"""
|
||||||
|
计算刀具房数量、线边刀库数量、机内刀库数量
|
||||||
|
"""
|
||||||
|
for tool in self:
|
||||||
|
if tool:
|
||||||
|
tool.tool_stock_num = 0
|
||||||
|
tool.side_shelf_num = 0
|
||||||
|
tool.on_tool_stock_num = 0
|
||||||
|
if tool.sf_functional_tool_entity_ids:
|
||||||
|
for cutting_tool in tool.sf_functional_tool_entity_ids:
|
||||||
|
if cutting_tool.tool_room_num > 0:
|
||||||
|
tool.tool_stock_num += 1
|
||||||
|
elif cutting_tool.line_edge_knife_library_num > 0:
|
||||||
|
tool.side_shelf_num += 1
|
||||||
|
elif cutting_tool.machine_knife_library_num > 0:
|
||||||
|
tool.on_tool_stock_num += 1
|
||||||
|
else:
|
||||||
|
tool.tool_stock_num = 0
|
||||||
|
tool.side_shelf_num = 0
|
||||||
|
tool.on_tool_stock_num = 0
|
||||||
|
|
||||||
|
@api.depends('tool_stock_num', 'side_shelf_num', 'on_tool_stock_num')
|
||||||
|
def _compute_tool_stock_total(self):
|
||||||
|
for tool in self:
|
||||||
|
if tool:
|
||||||
|
# 计算当前库存量
|
||||||
|
tool.tool_stock_total = tool.tool_stock_num + tool.side_shelf_num + tool.on_tool_stock_num
|
||||||
|
|
||||||
def create_functional_tool_assembly(self, tool):
|
def create_functional_tool_assembly(self, tool):
|
||||||
"""
|
"""
|
||||||
创建功能刀具组装单
|
创建功能刀具组装单
|
||||||
@@ -516,27 +546,6 @@ class RealTimeDistributionOfFunctionalTools(models.Model):
|
|||||||
})
|
})
|
||||||
tool.sudo().sf_functional_tool_assembly_ids = [(4, functional_tool_assembly.id)]
|
tool.sudo().sf_functional_tool_assembly_ids = [(4, functional_tool_assembly.id)]
|
||||||
|
|
||||||
def get_stock_num(self, tool):
|
|
||||||
"""
|
|
||||||
计算刀具房数量、线边刀库数量、机内刀库数量
|
|
||||||
"""
|
|
||||||
if tool:
|
|
||||||
tool.tool_stock_num = 0
|
|
||||||
tool.side_shelf_num = 0
|
|
||||||
tool.on_tool_stock_num = 0
|
|
||||||
if tool.sf_functional_tool_entity_ids:
|
|
||||||
for cutting_tool in tool.sf_functional_tool_entity_ids:
|
|
||||||
if cutting_tool.tool_room_num > 0:
|
|
||||||
tool.tool_stock_num += 1
|
|
||||||
elif cutting_tool.line_edge_knife_library_num > 0:
|
|
||||||
tool.side_shelf_num += 1
|
|
||||||
elif cutting_tool.machine_knife_library_num > 0:
|
|
||||||
tool.on_tool_stock_num += 1
|
|
||||||
else:
|
|
||||||
tool.tool_stock_num = 0
|
|
||||||
tool.side_shelf_num = 0
|
|
||||||
tool.on_tool_stock_num = 0
|
|
||||||
|
|
||||||
def create_or_edit_safety_stock(self, vals, sf_functional_tool_entity_ids):
|
def create_or_edit_safety_stock(self, vals, sf_functional_tool_entity_ids):
|
||||||
"""
|
"""
|
||||||
根据传入的信息新增或者更新功能刀具安全库存的信息
|
根据传入的信息新增或者更新功能刀具安全库存的信息
|
||||||
|
|||||||
@@ -44,73 +44,79 @@ class SfMaintenanceEquipment(models.Model):
|
|||||||
|
|
||||||
# ==========机床当前刀库实时信息接口==========
|
# ==========机床当前刀库实时信息接口==========
|
||||||
def register_equipment_tool(self):
|
def register_equipment_tool(self):
|
||||||
config = self.env['res.config.settings'].get_values()
|
try:
|
||||||
# token = sf_sync_config['token'Ba F2CF5DCC-1A00-4234-9E95-65603F70CC8A]
|
config = self.env['res.config.settings'].get_values()
|
||||||
headers = {'Authorization': config['center_control_Authorization']}
|
# token = sf_sync_config['token'Ba F2CF5DCC-1A00-4234-9E95-65603F70CC8A]
|
||||||
crea_url = config['center_control_url'] + "/AutoDeviceApi/GetToolInfos"
|
headers = {'Authorization': config['center_control_Authorization']}
|
||||||
params = {"DeviceId": self.name}
|
crea_url = config['center_control_url'] + "/AutoDeviceApi/GetToolInfos"
|
||||||
r = requests.get(crea_url, params=params, headers=headers)
|
params = {"DeviceId": self.name}
|
||||||
ret = r.json()
|
r = requests.get(crea_url, params=params, headers=headers)
|
||||||
logging.info('register_equipment_tool:%s' % ret)
|
ret = r.json()
|
||||||
datas = ret['Datas']
|
logging.info('register_equipment_tool:%s' % ret)
|
||||||
self.write_maintenance_equipment_tool(datas)
|
datas = ret['Datas']
|
||||||
if ret['Succeed']:
|
self.write_maintenance_equipment_tool(datas)
|
||||||
return "机床当前刀库实时信息指令发送成功"
|
if ret['Succeed']:
|
||||||
else:
|
return "机床当前刀库实时信息指令发送成功"
|
||||||
raise ValidationError("机床当前刀库实时信息指令发送失败")
|
else:
|
||||||
|
raise ValidationError("机床当前刀库实时信息指令发送失败")
|
||||||
|
except Exception as e:
|
||||||
|
logging.info("register_equipment_tool()捕获错误信息:%s" % e)
|
||||||
|
|
||||||
def write_maintenance_equipment_tool(self, datas):
|
def write_maintenance_equipment_tool(self, datas):
|
||||||
if datas:
|
try:
|
||||||
# 清除设备机床刀位的刀具信息
|
if datas:
|
||||||
for obj in self.product_template_ids:
|
# 清除设备机床刀位的刀具信息
|
||||||
obj.write({
|
for obj in self.product_template_ids:
|
||||||
'functional_tool_name_id': False,
|
obj.write({
|
||||||
'tool_install_time': None
|
'functional_tool_name_id': False,
|
||||||
})
|
'tool_install_time': None
|
||||||
for data in datas:
|
})
|
||||||
maintenance_equipment_id = self.search([('name', '=', data['DeviceId'])])
|
for data in datas:
|
||||||
if maintenance_equipment_id:
|
maintenance_equipment_id = self.search([('name', '=', data['DeviceId'])])
|
||||||
tool_id = '%s%s' % (data['ToolId'][0:1], data['ToolId'][1:].zfill(2))
|
if maintenance_equipment_id:
|
||||||
equipment_tool_id = self.env['maintenance.equipment.tool'].sudo().search(
|
tool_id = '%s%s' % (data['ToolId'][0:1], data['ToolId'][1:].zfill(2))
|
||||||
[('equipment_id', '=', maintenance_equipment_id.id), ('code', '=', tool_id)])
|
equipment_tool_id = self.env['maintenance.equipment.tool'].sudo().search(
|
||||||
functional_tool_id = self.env['sf.functional.cutting.tool.entity'].sudo().search(
|
[('equipment_id', '=', maintenance_equipment_id.id), ('code', '=', tool_id)])
|
||||||
[('rfid', '=', data['RfidCode'])])
|
functional_tool_id = self.env['sf.functional.cutting.tool.entity'].sudo().search(
|
||||||
if functional_tool_id:
|
[('rfid', '=', data['RfidCode'])])
|
||||||
if len(functional_tool_id) > 1:
|
if functional_tool_id:
|
||||||
functional_tool_id = functional_tool_id[-1]
|
if len(functional_tool_id) > 1:
|
||||||
# 查询该功能刀具是否已经装在机床内其他位置,如果是就删除
|
functional_tool_id = functional_tool_id[-1]
|
||||||
equipment_tools = self.env['maintenance.equipment.tool'].sudo().search(
|
# 查询该功能刀具是否已经装在机床内其他位置,如果是就删除
|
||||||
[('functional_tool_name_id', '=', functional_tool_id.id), ('code', '!=', tool_id)])
|
equipment_tools = self.env['maintenance.equipment.tool'].sudo().search(
|
||||||
if equipment_tools:
|
[('functional_tool_name_id', '=', functional_tool_id.id), ('code', '!=', tool_id)])
|
||||||
for item in equipment_tools:
|
if equipment_tools:
|
||||||
item.write({
|
for item in equipment_tools:
|
||||||
'functional_tool_name_id': False,
|
item.write({
|
||||||
'tool_install_time': None
|
'functional_tool_name_id': False,
|
||||||
})
|
'tool_install_time': None
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logging.info('Rfid为【%s】的功能刀具不存在!' % data['RfidCode'])
|
||||||
|
time = None
|
||||||
|
if data['AddDatetime']:
|
||||||
|
datatime = str(data['AddDatetime'])
|
||||||
|
time = fields.Datetime.from_string(datatime[0:10] + ' ' + datatime[11:19])
|
||||||
|
if equipment_tool_id and functional_tool_id:
|
||||||
|
tool_install_time = {'Nomal': '正常', 'Warning': '报警'}
|
||||||
|
equipment_tool_id.write({
|
||||||
|
'functional_tool_name_id': functional_tool_id.id,
|
||||||
|
'tool_install_time': time
|
||||||
|
})
|
||||||
|
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'])
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
logging.info('Rfid为【%s】的功能刀具不存在!' % data['RfidCode'])
|
logging.info('获取的【%s】设备不存在!!!' % data['DeviceId'])
|
||||||
time = None
|
else:
|
||||||
if data['AddDatetime']:
|
logging.info('没有获取到【%s】设备的刀具库信息!!!' % self.name)
|
||||||
datatime = str(data['AddDatetime'])
|
except Exception as e:
|
||||||
time = fields.Datetime.from_string(datatime[0:10] + ' ' + datatime[11:19])
|
logging.info("write_maintenance_equipment_tool()捕获错误信息:%s" % e)
|
||||||
if equipment_tool_id and functional_tool_id:
|
|
||||||
tool_install_time = {'Nomal': '正常', 'Warning': '报警'}
|
|
||||||
equipment_tool_id.write({
|
|
||||||
'functional_tool_name_id': functional_tool_id.id,
|
|
||||||
'tool_install_time': time
|
|
||||||
})
|
|
||||||
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'])
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
logging.info('获取的【%s】设备不存在!!!' % data['DeviceId'])
|
|
||||||
else:
|
|
||||||
logging.info('没有获取到【%s】设备的刀具库信息!!!' % self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class StockLot(models.Model):
|
class StockLot(models.Model):
|
||||||
|
|||||||
22
sf_tool_management/models/stock.py
Normal file
22
sf_tool_management/models/stock.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from odoo import api, fields, models, _
|
||||||
|
|
||||||
|
|
||||||
|
class ShelfLocation(models.Model):
|
||||||
|
_inherit = 'sf.shelf.location'
|
||||||
|
|
||||||
|
tool_rfid = fields.Char('Rfid', compute='_compute_tool', store=True)
|
||||||
|
tool_name_id = fields.Many2one('sf.functional.cutting.tool.entity', string='功能刀具名称', compute='_compute_tool',
|
||||||
|
store=True)
|
||||||
|
|
||||||
|
@api.depends('product_id')
|
||||||
|
def _compute_tool(self):
|
||||||
|
for item in self:
|
||||||
|
if item.product_id:
|
||||||
|
if item.product_id.categ_id.name == '功能刀具':
|
||||||
|
tool_id = self.env['sf.functional.cutting.tool.entity'].sudo().search(
|
||||||
|
[('barcode_id', '=', item.product_sn_id.id)])
|
||||||
|
item.tool_rfid = tool_id.rfid
|
||||||
|
item.tool_name_id = tool_id.id
|
||||||
|
return True
|
||||||
|
item.tool_rfid = ''
|
||||||
|
item.tool_name_id = False
|
||||||
14
sf_tool_management/views/stock.xml
Normal file
14
sf_tool_management/views/stock.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_shelf_location_tool" model="ir.ui.view">
|
||||||
|
<field name="name">sf.shelf.location.form.tool</field>
|
||||||
|
<field name="model">sf.shelf.location</field>
|
||||||
|
<field name="inherit_id" ref="sf_warehouse.view_shelf_location_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='product_id']" position="after">
|
||||||
|
<field name="tool_rfid" attrs="{'invisible': [('tool_rfid','=',False)]}"/>
|
||||||
|
<field name="tool_name_id" attrs="{'invisible': [('tool_name_id','=',False)]}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user