diff --git a/sf_hr/__init__.py b/sf_hr/__init__.py
new file mode 100644
index 00000000..cde864ba
--- /dev/null
+++ b/sf_hr/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import models
diff --git a/sf_hr/__manifest__.py b/sf_hr/__manifest__.py
new file mode 100644
index 00000000..aaf9cfc7
--- /dev/null
+++ b/sf_hr/__manifest__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': '机企猫智能工厂 员工管理',
+ 'version': '1.0',
+ 'summary': '智能工厂员工模块',
+ 'sequence': 1,
+ 'category': 'sf',
+ 'website': 'https://www.sf.jikimo.com',
+ 'depends': ['hr'],
+ 'data': [
+ 'views/hr_employee.xml',
+ ],
+ 'demo': [
+ ],
+ 'qweb': [
+ ],
+ 'license': 'LGPL-3',
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+}
diff --git a/sf_hr/models/__init__.py b/sf_hr/models/__init__.py
new file mode 100644
index 00000000..633f8661
--- /dev/null
+++ b/sf_hr/models/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+
diff --git a/sf_hr/security/ir.model.access.csv b/sf_hr/security/ir.model.access.csv
new file mode 100644
index 00000000..e69de29b
diff --git a/sf_hr/views/hr_employee.xml b/sf_hr/views/hr_employee.xml
new file mode 100644
index 00000000..a3db9076
--- /dev/null
+++ b/sf_hr/views/hr_employee.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ employee_form
+ hr.employee
+
+
+
+ 1
+
+
+
+
+
\ No newline at end of file
diff --git a/sf_machine_connect/controllers/controllers.py b/sf_machine_connect/controllers/controllers.py
index 94c0fdcb..96708dd8 100644
--- a/sf_machine_connect/controllers/controllers.py
+++ b/sf_machine_connect/controllers/controllers.py
@@ -173,49 +173,6 @@ class Sf_Dashboard_Connect(http.Controller):
res['message'] = '前端请求机床数据失败,原因:%s' % e
return json.JSONEncoder().encode(res)
- # @http.route('/api/logs/list', type='http', auth='public', methods=['GET', 'POST'], csrf=False,
- # cors="*")
- # def logs_list(self, **kw):
- # """
- # 拿到日志数据返回给大屏展示
- # :param kw:
- # :return:
- # """
- # res = {'status': 1, 'message': '成功', 'data': []}
- # logging.info('前端请求日志数据的参数为:%s' % kw)
- #
- # try:
- # # 获取请求的日志数据
- # logs_obj = request.env['maintenance.equipment.oee.log.detail'].sudo()
- # # 获取请求的机床数据
- # machine_list = ast.literal_eval(kw['machine_list'])
- # begin_time_str = kw['begin_time'].strip('"')
- # end_time_str = kw['end_time'].strip('"')
- #
- # begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S')
- # end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S')
- #
- # print('begin_time: %s' % begin_time)
- # for item in machine_list:
- # log_datas = logs_obj.search(
- # [('equipment_code', '=', item), ('time', '>=', begin_time), ('time', '<=', end_time)])
- # print('log_datas: %s' % log_datas)
- # for log_data in log_datas:
- # res['data'].append({
- # 'equipment_code': log_data.equipment_code,
- # 'time': log_data.time.strftime('%Y-%m-%d %H:%M:%S'),
- # 'state': log_data.state
- #
- # })
- #
- # return json.JSONEncoder().encode(res)
- #
- # except Exception as e:
- # logging.info('前端请求日志数据失败,原因:%s' % e)
- # res['status'] = -1
- # res['message'] = '前端请求日志数据失败,原因:%s' % e
- # return json.JSONEncoder().encode(res)
-
@http.route('/api/logs/list', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
def logs_list(self, **kw):
"""
@@ -227,9 +184,9 @@ class Sf_Dashboard_Connect(http.Controller):
logging.info('前端请求日志数据的参数为:%s' % kw)
try:
- # 获取请求的日志数据
- logs_obj = request.env['maintenance.equipment.oee.log.detail'].sudo()
- # 获取请求的机床数据
+ # 连接数据库
+ conn = psycopg2.connect(**db_config)
+ cur = conn.cursor()
machine_list = ast.literal_eval(kw['machine_list'])
begin_time_str = kw['begin_time'].strip('"')
end_time_str = kw['end_time'].strip('"')
@@ -240,19 +197,25 @@ class Sf_Dashboard_Connect(http.Controller):
print('begin_time: %s' % begin_time)
for item in machine_list:
- log_datas = logs_obj.search(
- [('equipment_code', '=', item), ('time', '>=', begin_time), ('time', '<=', end_time)])
- print('log_datas: %s' % log_datas)
+ sql = '''
+ SELECT time, device_state, program_name
+ FROM device_data
+ WHERE device_name = %s AND time >= %s AND time <= %s
+ ORDER BY time ASC;
+ '''
+ # 执行SQL命令,使用参数绑定
+ cur.execute(sql, (item, begin_time, end_time))
+ results = cur.fetchall()
# 将数据按照 equipment_code 进行分组
if item not in res['data']:
res['data'][item] = []
- for log_data in log_datas:
+ for result in results:
res['data'][item].append({
- 'time': log_data.time.strftime('%Y-%m-%d %H:%M:%S'),
- 'state': log_data.state,
- 'production_name': log_data.production_name,
+ 'time': result[0].strftime('%Y-%m-%d %H:%M:%S'),
+ 'state': result[1],
+ 'production_name': result[2],
})
return json.dumps(res) # 注意使用 json.dumps 而不是直接用 json.JSONEncoder().encode()
@@ -624,7 +587,7 @@ class Sf_Dashboard_Connect(http.Controller):
# 查询pg库来获得待机次数
@http.route('/api/IdleAlarmCount', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
- def idle_count(self, **kw):
+ def idle_alarm_count(self, **kw):
"""
查询设备的待机次数
"""
@@ -637,69 +600,50 @@ class Sf_Dashboard_Connect(http.Controller):
try:
# 获取请求的机床数据
machine_list = ast.literal_eval(kw['machine_list'])
- idle_times = []
- idle_dict = {}
-
+ total_alarm_time = 0
+ alarm_count_num = 0
for item in machine_list:
sql = '''
- SELECT idle_start_time,alarm_time,alarm_repair_time FROM device_data WHERE device_name = %s;
+ SELECT COUNT(*)
+ FROM (
+ SELECT DISTINCT ON (idle_start_time) idle_start_time
+ FROM device_data
+ WHERE device_name = %s AND idle_start_time IS NOT NULL
+ ORDER BY idle_start_time, time
+ ) subquery;
+ '''
+
+ sql2 = '''
+ SELECT DISTINCT ON (alarm_time) alarm_time, alarm_repair_time
+ FROM device_data
+ WHERE device_name = %s AND alarm_time IS NOT NULL
+ ORDER BY alarm_time, time;
+
'''
# 执行SQL命令
cur.execute(sql, (item,))
result = cur.fetchall()
- # # print('result', result)
- #
- # # 将查询结果添加到idle_times列表中
- # idle_times = [row[0] for row in result if row[0] is not None]
- #
- # # 对结果去重
- # unique_idle_times = set(idle_times)
- # # print('unique_idle_times', unique_idle_times)
- #
- # # 统计去重后的数量
- # idle_count = len(unique_idle_times)
- # # idle_dict[item] = idle_count
- #
- # res['data'][item] = idle_count
-
- total_alarm_time = 0
- alarm_count = 0
- alarm_time_list = []
- idle_times = []
- alarm_times = []
+ print('result========', result)
+ cur.execute(sql2, (item,))
+ result2 = cur.fetchall()
+ print('result2========', result2)
+ #
for row in result:
- idle_start_time = row[0]
- alarm_time = row[1]
- alarm_repair_time = row[2]
-
- alarm_time_list.append(alarm_time) # 将时长累加,以秒为单位
- idle_times.append(idle_start_time)
- # if alarm_repair_time is not None:
- # alarm_times.append(alarm_repair_time)
- alarm_times.append(alarm_repair_time)
-
- # 对结果去重
- unique_total_alarm_time = set(alarm_time_list)
- unique_idle_times = set(idle_times)
- unique_alarm_times = set(alarm_times)
-
- # 统计去重后的数量
- idle_count = len(unique_idle_times)
-
- for alarm_time in unique_total_alarm_time:
- if alarm_time is not None:
- total_alarm_time += abs(float(alarm_time))
-
- alarm_count = len(unique_alarm_times) if unique_alarm_times else 0
- alarm_count = alarm_count if total_alarm_time else 0
-
- # 存储待机次数和总待机时长(单位:秒)
- res['data'][item] = {
- 'idle_count': idle_count,
- 'total_alarm_time': total_alarm_time / 3600, # 以秒为单位
- 'alarm_count': alarm_count
- }
+ res['data'][item] = {'idle_count': row[0]}
+ alarm_count = []
+ for row in result2:
+ alarm_count.append(row[0])
+ total_alarm_time += abs(float(row[0]))
+ if len(list(set(alarm_count))) == 1:
+ if list(set(alarm_count))[0] is None:
+ alarm_count_num = 0
+ else:
+ alarm_count_num = 1
+ else:
+ alarm_count_num = len(list(set(alarm_count)))
+ res['data'][item]['total_alarm_time'] = total_alarm_time / 3600
+ res['data'][item]['alarm_count_num'] = alarm_count_num
# 返回统计结果
return json.dumps(res)
@@ -712,7 +656,7 @@ class Sf_Dashboard_Connect(http.Controller):
# 查询pg库来获得异常情况
@http.route('/api/alarm/logs', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
- def idle_count(self, **kw):
+ def alarm_logs(self, **kw):
"""
查询设备的异常情况
"""
@@ -764,3 +708,110 @@ class Sf_Dashboard_Connect(http.Controller):
finally:
cur.close()
conn.close()
+
+ # 设备oee
+ @http.route('/api/OEE', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
+ def OEE(self, **kw):
+ """
+ 获取产线等oee
+ """
+ res = {'status': 1, 'message': '成功', 'data': {}}
+ logging.info('前端请求oee数据的参数为:%s' % kw)
+
+ try:
+ count_oee = 1
+ workcenter_obj = request.env['mrp.workcenter'].sudo()
+ workcenter_list = ast.literal_eval(kw['workcenter_list'])
+ print('workcenter_list: %s' % workcenter_list)
+ for line in workcenter_list:
+ res['data'][line] = workcenter_obj.search([('name', '=', line)]).oee
+ count_oee *= workcenter_obj.search([('name', '=', line)]).oee
+ res['data']['综合oee'] = count_oee / 1000000
+ except Exception as e:
+ print(f"An error occurred: {e}")
+
+ return json.dumps(res)
+
+ # # 查询某段时间的设备oee
+ # @http.route('/api/OEEByTime', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
+ # def OEEByTime(self, **kw):
+ # """
+ # 获取某段时间的oee
+ # """
+ # res = {'status': 1, 'message': '成功', 'data': {}}
+ # logging.info('前端请求获取某段时间的oee的参数为:%s' % kw)
+ # workcenter_list = ast.literal_eval(kw['workcenter_list'])
+ # begin_time_str = kw['begin_time'].strip('"')
+ # end_time_str = kw['end_time'].strip('"')
+ # begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S')
+ # end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S')
+ # print('workcenter_list: %s' % workcenter_list)
+ # # 连接数据库
+ # conn = psycopg2.connect(**db_config)
+ # cur = conn.cursor()
+ # # 查询并计算OEE平均值
+ # oee_data = {}
+ # for workcenter in workcenter_list:
+ # cur.execute("""
+ # SELECT AVG(oee) as avg_oee
+ # FROM oee_data
+ # WHERE workcenter_name = %s
+ # AND time BETWEEN %s AND %s
+ # """, (workcenter, begin_time, end_time))
+ #
+ # result = cur.fetchone()
+ # avg_oee = result[0] if result else 0.0
+ # oee_data[workcenter] = avg_oee
+ #
+ # # 返回数据
+ # res['data'] = oee_data
+ # return json.dumps(res)
+
+ @http.route('/api/OEEByTime', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
+ def OEEByTime(self, **kw):
+ """
+ 获取某段时间的oee,根据用户指定的时间单位(day或hour)返回对应的平均值
+ """
+ res = {'status': 1, 'message': '成功', 'data': {}}
+ logging.info('前端请求获取某段时间的oee的参数为:%s' % kw)
+
+ # 获取并解析参数
+ workcenter_list = ast.literal_eval(kw['workcenter_list'])
+ begin_time_str = kw['begin_time'].strip('"')
+ end_time_str = kw['end_time'].strip('"')
+ time_unit = kw.get('time_unit', 'day') # 默认单位为天
+ begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S')
+ end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S')
+
+ # 连接数据库
+ conn = psycopg2.connect(**db_config)
+ cur = conn.cursor()
+
+ # 根据时间单位选择不同的时间格式
+ if time_unit == 'hour':
+ time_format = 'YYYY-MM-DD HH24:00:00'
+ else: # 默认为'day'
+ time_format = 'YYYY-MM-DD'
+
+ # 查询并计算OEE平均值
+ oee_data = {}
+ for workcenter in workcenter_list:
+ cur.execute(f"""
+ SELECT to_char(time, '{time_format}') as time_unit, AVG(oee) as avg_oee
+ FROM oee_data
+ WHERE workcenter_name = %s
+ AND time BETWEEN %s AND %s
+ GROUP BY time_unit
+ ORDER BY time_unit
+ """, (workcenter, begin_time, end_time))
+
+ results = cur.fetchall()
+ oee_data[workcenter] = {row[0]: row[1] for row in results}
+
+ # 关闭数据库连接
+ cur.close()
+ conn.close()
+
+ # 返回数据
+ res['data'] = oee_data
+ return json.dumps(res)
diff --git a/sf_manufacturing/__manifest__.py b/sf_manufacturing/__manifest__.py
index e84b34cc..35501367 100644
--- a/sf_manufacturing/__manifest__.py
+++ b/sf_manufacturing/__manifest__.py
@@ -32,6 +32,7 @@
'views/model_type_view.xml',
'views/agv_setting_views.xml',
'views/sf_maintenance_equipment.xml',
+ 'views/res_config_settings_views.xml',
],
'assets': {
diff --git a/sf_manufacturing/models/__init__.py b/sf_manufacturing/models/__init__.py
index b93d613e..7d6aa8ae 100644
--- a/sf_manufacturing/models/__init__.py
+++ b/sf_manufacturing/models/__init__.py
@@ -10,3 +10,4 @@ from . import res_user
from . import production_line_base
from . import agv_setting
from . import agv_scheduling
+from . import res_config_setting
diff --git a/sf_manufacturing/models/agv_scheduling.py b/sf_manufacturing/models/agv_scheduling.py
index 237038ba..d232c643 100644
--- a/sf_manufacturing/models/agv_scheduling.py
+++ b/sf_manufacturing/models/agv_scheduling.py
@@ -231,7 +231,9 @@ class AgvScheduling(models.Model):
rec.site_state = '空闲'
rec.end_site_id = agv_task_route.end_site_id.id
rec.agv_route_id = agv_task_route.id
- # rec._delivery_avg()
+ is_agv_task_dispatch = self.env['ir.config_parameter'].sudo().get_param('is_agv_task_dispatch')
+ if is_agv_task_dispatch:
+ rec._delivery_avg()
# 更新接驳站状态
rec.env['sf.agv.site'].update_site_state({rec.end_site_id.name: '占用'}, False)
diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py
index 8e1f7199..322dff15 100644
--- a/sf_manufacturing/models/mrp_production.py
+++ b/sf_manufacturing/models/mrp_production.py
@@ -123,7 +123,12 @@ class MrpProduction(models.Model):
# 上传零件图纸
part_drawing = fields.Binary('零件图纸')
- manual_quotation = fields.Boolean('人工编程', default=False, readonly=True)
+ @api.depends('product_id.manual_quotation')
+ def _compute_manual_quotation(self):
+ for item in self:
+ item.manual_quotation = item.product_id.manual_quotation
+
+ manual_quotation = fields.Boolean('人工编程', default=False, compute=_compute_manual_quotation, store=True)
is_scrap = fields.Boolean('是否报废', default=False)
is_remanufacture = fields.Boolean('是否重新制造', default=False)
diff --git a/sf_manufacturing/models/mrp_workcenter.py b/sf_manufacturing/models/mrp_workcenter.py
index 461ef6ad..03597980 100644
--- a/sf_manufacturing/models/mrp_workcenter.py
+++ b/sf_manufacturing/models/mrp_workcenter.py
@@ -41,6 +41,7 @@ class ResWorkcenter(models.Model):
oee_target = fields.Float(
string='OEE Target', help="Overall Effective Efficiency Target in percentage", default=90, tracking=True)
+ oee = fields.Float(compute='_compute_oee', help='Overall Equipment Effectiveness, based on the last month', store=True)
time_start = fields.Float('Setup Time', tracking=True)
time_stop = fields.Float('Cleanup Time', tracking=True)
diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py
index 095c1ab1..d6a9b7a4 100644
--- a/sf_manufacturing/models/mrp_workorder.py
+++ b/sf_manufacturing/models/mrp_workorder.py
@@ -60,7 +60,12 @@ class ResMrpWorkOrder(models.Model):
default='pending', copy=False, readonly=True, recursive=True, index=True, tracking=True)
# state = fields.Selection(selection_add=[('to be detected', "待检测"), ('rework', '返工')], tracking=True)
- manual_quotation = fields.Boolean('人工编程', default=False, readonly=True)
+ @api.depends('production_id.manual_quotation')
+ def _compute_manual_quotation(self):
+ for item in self:
+ item.manual_quotation = item.production_id.manual_quotation
+
+ manual_quotation = fields.Boolean('人工编程', default=False, compute=_compute_manual_quotation, store=True)
def _compute_working_users(self):
super()._compute_working_users()
@@ -1154,10 +1159,10 @@ class ResMrpWorkOrder(models.Model):
def button_finish(self):
for record in self:
if record.routing_type == '装夹预调':
- if not record.material_center_point or record.X_deviation_angle <= 0:
- raise UserError("请对前置三元检测定位参数进行计算定位")
if not record.rfid_code and record.is_rework is False:
raise UserError("请扫RFID码进行绑定")
+ if not record.material_center_point or record.X_deviation_angle <= 0:
+ raise UserError("请对前置三元检测定位参数进行计算定位")
record.process_state = '待加工'
# record.write({'process_state': '待加工'})
record.production_id.process_state = '待加工'
diff --git a/sf_manufacturing/models/res_config_setting.py b/sf_manufacturing/models/res_config_setting.py
new file mode 100644
index 00000000..d6b029c6
--- /dev/null
+++ b/sf_manufacturing/models/res_config_setting.py
@@ -0,0 +1,22 @@
+from odoo import models, fields, api
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ is_agv_task_dispatch = fields.Boolean('是否下发AGV任务', default=False)
+
+ @api.model
+ def get_values(self):
+ values = super(ResConfigSettings, self).get_values()
+ config = self.env['ir.config_parameter'].sudo()
+ is_agv_task_dispatch = config.get_param('is_agv_task_dispatch')
+ values.update(
+ is_agv_task_dispatch=is_agv_task_dispatch,
+ )
+ return values
+
+ def set_values(self):
+ super(ResConfigSettings, self).set_values()
+ config = self.env['ir.config_parameter'].sudo()
+ config.set_param("is_agv_task_dispatch", self.is_agv_task_dispatch or False)
diff --git a/sf_manufacturing/views/res_config_settings_views.xml b/sf_manufacturing/views/res_config_settings_views.xml
new file mode 100644
index 00000000..89738492
--- /dev/null
+++ b/sf_manufacturing/views/res_config_settings_views.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ res.config.settings.view.form.inherit.sf_sync
+ res.config.settings
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sf_manufacturing/wizard/workpiece_delivery_wizard.py b/sf_manufacturing/wizard/workpiece_delivery_wizard.py
index 8e7f47f8..5d5dc889 100644
--- a/sf_manufacturing/wizard/workpiece_delivery_wizard.py
+++ b/sf_manufacturing/wizard/workpiece_delivery_wizard.py
@@ -109,11 +109,19 @@ class WorkpieceDeliveryWizard(models.TransientModel):
)
# 如果关联了工件配送单,则修改状态为已下发
if self.delivery_ids:
- self.delivery_ids.write({
+ val = {
'status': '已下发',
'agv_scheduling_id': scheduling.id,
'feeder_station_start_id': scheduling.start_site_id.id,
- })
+ }
+ # 如果agv任务已经下发,则修改工件配送单信息
+ if scheduling.state == '配送中':
+ val.update({
+ 'feeder_station_destination_id': scheduling.end_site_id.id,
+ 'route_id': scheduling.agv_route_id.id,
+ 'task_delivery_time': fields.Datetime.now()
+ })
+ self.delivery_ids.write(val)
# 如果是解除装夹工单,则需要处理工单逻辑
for item in self.workorder_ids:
diff --git a/sf_mrs_connect/views/res_config_settings_views.xml b/sf_mrs_connect/views/res_config_settings_views.xml
index 08e9560f..05e5be41 100644
--- a/sf_mrs_connect/views/res_config_settings_views.xml
+++ b/sf_mrs_connect/views/res_config_settings_views.xml
@@ -76,7 +76,7 @@
AGV参数配置
-
+
diff --git a/sf_sale/views/sale_order_view.xml b/sf_sale/views/sale_order_view.xml
index 73996954..534d42e4 100644
--- a/sf_sale/views/sale_order_view.xml
+++ b/sf_sale/views/sale_order_view.xml
@@ -130,6 +130,9 @@
{'readonly': [('state', 'in', ['cancel','sale'])]}
+
+ 拒绝接单
+