Merge branch 'release/release_2.11'

This commit is contained in:
胡尧
2025-04-15 20:54:53 +08:00
22 changed files with 732 additions and 141 deletions

View File

@@ -206,7 +206,7 @@ class QualityCheck(models.Model):
('NG', 'NG') ('NG', 'NG')
], string='出厂检验报告结果', default='OK') ], string='出厂检验报告结果', default='OK')
measure_operator = fields.Many2one('res.users', string='操机员') measure_operator = fields.Many2one('res.users', string='操机员')
quality_manager = fields.Many2one('res.users', string='质检员', compute='_compute_quality_manager', store=True) quality_manager = fields.Many2one('res.users', string='质检员', compute='_compute_quality_manager')
@api.depends('measure_line_ids') @api.depends('measure_line_ids')
def _compute_quality_manager(self): def _compute_quality_manager(self):

View File

@@ -267,7 +267,7 @@
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
<field name="categ_type" invisible="1"/> <field name="categ_type" invisible="1"/>
<field name="product_id" attrs="{'invisible' : [('measure_on', '=', 'operation')]}"/> <field name="product_id" attrs="{'invisible' : [('measure_on', '=', 'operation')]}"/>
<field name="part_name" attrs="{'invisible': [('categ_type', '!=', '成品')]}"/> <field name="part_name" attrs="{'invisible': [('categ_type', '!=', '成品')], 'readonly': [('publish_status', '=', 'published')]}"/>
<field name="part_number" attrs="{'invisible': [('categ_type', '!=', '成品')], 'readonly': [('publish_status', '=', 'published')]}"/> <field name="part_number" attrs="{'invisible': [('categ_type', '!=', '成品')], 'readonly': [('publish_status', '=', 'published')]}"/>
<field name="material_name" attrs="{'invisible': [('categ_type', '!=', '成品')]}"/> <field name="material_name" attrs="{'invisible': [('categ_type', '!=', '成品')]}"/>
<field name="total_qty" attrs="{'invisible': ['|', ('measure_on', '!=', 'product'), ('is_out_check', '=', False)]}"/> <field name="total_qty" attrs="{'invisible': ['|', ('measure_on', '!=', 'product'), ('is_out_check', '=', False)]}"/>

View File

@@ -5,7 +5,7 @@ import json
import base64 import base64
import logging import logging
import psycopg2 import psycopg2
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from odoo import http, fields from odoo import http, fields
from odoo.http import request from odoo.http import request
@@ -414,7 +414,7 @@ class Sf_Dashboard_Connect(http.Controller):
# 工单计划量切换为CNC工单 # 工单计划量切换为CNC工单
plan_data_total_counts = work_order_obj.search_count( plan_data_total_counts = work_order_obj.search_count(
[('production_id.production_line_id.name', '=', line), [('production_line_id.name', '=', line), ('id', '!=', 8061),
('state', 'in', ['ready', 'progress', 'done']), ('routing_type', '=', 'CNC加工')]) ('state', 'in', ['ready', 'progress', 'done']), ('routing_type', '=', 'CNC加工')])
# # 工单完成量 # # 工单完成量
@@ -423,13 +423,13 @@ class Sf_Dashboard_Connect(http.Controller):
# 工单完成量切换为CNC工单 # 工单完成量切换为CNC工单
plan_data_finish_counts = work_order_obj.search_count( plan_data_finish_counts = work_order_obj.search_count(
[('production_id.production_line_id.name', '=', line), [('production_line_id.name', '=', line),
('state', 'in', ['done']), ('routing_type', '=', 'CNC加工')]) ('state', 'in', ['done']), ('routing_type', '=', 'CNC加工')])
# 超期完成量 # 超期完成量
# 搜索所有已经完成的工单 # 搜索所有已经完成的工单
plan_data_overtime = work_order_obj.search([ plan_data_overtime = work_order_obj.search([
('production_id.production_line_id.name', '=', line), ('production_line_id.name', '=', line),
('state', 'in', ['done']), ('state', 'in', ['done']),
('routing_type', '=', 'CNC加工') ('routing_type', '=', 'CNC加工')
]) ])
@@ -448,9 +448,14 @@ class Sf_Dashboard_Connect(http.Controller):
]) ])
# 过滤出那些检测结果状态为 '返工' 或 '报废' 的记录 # 过滤出那些检测结果状态为 '返工' 或 '报废' 的记录
faulty_plans = plan_data.filtered(lambda p: any( # faulty_plans = plan_data.filtered(lambda p: any(
result.test_results in ['返工', '报废'] for result in p.production_id.detection_result_ids # result.test_results in ['返工', '报废'] for result in p.production_id.detection_result_ids
)) # ))
faulty_plans = request.env['quality.check'].sudo().search([
('operation_id.name', '=', 'CNC加工'),
('quality_state', 'in', ['fail'])
])
# 查找制造订单取消与归档的数量 # 查找制造订单取消与归档的数量
cancel_order_count = production_obj.search_count( cancel_order_count = production_obj.search_count(
@@ -567,7 +572,7 @@ class Sf_Dashboard_Connect(http.Controller):
""" """
res = {'status': 1, 'message': '成功', 'data': {}} res = {'status': 1, 'message': '成功', 'data': {}}
# plan_obj = request.env['sf.production.plan'].sudo() # plan_obj = request.env['sf.production.plan'].sudo()
plan_obj = request.env['mrp.workorder'].sudo().search([('routing_type', '=', 'CNC加工')]) # plan_obj = request.env['mrp.workorder'].sudo().search([('routing_type', '=', 'CNC加工')])
line_list = ast.literal_eval(kw['line_list']) line_list = ast.literal_eval(kw['line_list'])
begin_time_str = kw['begin_time'].strip('"') begin_time_str = kw['begin_time'].strip('"')
end_time_str = kw['end_time'].strip('"') end_time_str = kw['end_time'].strip('"')
@@ -617,11 +622,19 @@ class Sf_Dashboard_Connect(http.Controller):
for time_interval in time_intervals: for time_interval in time_intervals:
start_time, end_time = time_interval start_time, end_time = time_interval
orders = plan_obj.search([ # orders = plan_obj.search([
('production_id.production_line_id.name', '=', line), # ('production_line_id.name', '=', line),
# ('state', 'in', ['done']),
# (date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')),
# (date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) # 包括结束时间
# ])
orders = request.env['mrp.workorder'].sudo().search([
('routing_type', '=', 'CNC加工'), # 将第一个条件合并进来
('production_line_id.name', '=', line),
('state', 'in', ['done']), ('state', 'in', ['done']),
(date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')), (date_field_name, '>=', start_time.strftime('%Y-%m-%d %H:%M:%S')),
(date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S')) # 包括结束时间 (date_field_name, '<=', end_time.strftime('%Y-%m-%d %H:%M:%S'))
]) ])
# 使用小时和分钟作为键,确保每个小时的数据有独立的键 # 使用小时和分钟作为键,确保每个小时的数据有独立的键
@@ -638,18 +651,22 @@ class Sf_Dashboard_Connect(http.Controller):
for date in date_list: for date in date_list:
next_day = date + timedelta(days=1) next_day = date + timedelta(days=1)
orders = plan_obj.search([('production_id.production_line_id.name', '=', line), ('state', 'in', ['done']), orders = request.env['mrp.workorder'].sudo().search(
(date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), [('production_id.production_line_id.name', '=', line), ('state', 'in', ['done']),
(date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) ('routing_type', '=', 'CNC加工'),
])
rework_orders = plan_obj.search(
[('production_id.production_line_id.name', '=', line), ('state', 'in', ['rework']),
(date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')),
(date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00'))
]) ])
not_passed_orders = plan_obj.search(
rework_orders = request.env['mrp.workorder'].sudo().search(
[('production_id.production_line_id.name', '=', line), ('state', 'in', ['rework']),
('routing_type', '=', 'CNC加工'),
(date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')),
(date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00'))
])
not_passed_orders = request.env['mrp.workorder'].sudo().search(
[('production_id.production_line_id.name', '=', line), ('state', 'in', ['scrap', 'cancel']), [('production_id.production_line_id.name', '=', line), ('state', 'in', ['scrap', 'cancel']),
('routing_type', '=', 'CNC加工'),
(date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')),
(date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00'))
]) ])
@@ -751,11 +768,14 @@ class Sf_Dashboard_Connect(http.Controller):
for line in line_list: for line in line_list:
# 未完成订单 # 未完成订单
not_done_orders = plan_obj.search( # not_done_orders = plan_obj.search(
[('production_line_id.name', '=', line), ('state', 'not in', ['finished']), # [('production_line_id.name', '=', line), ('state', 'not in', ['finished']),
('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True) # ('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True)
# ])
not_done_orders = request.env['mrp.workorder'].sudo().search(
[('production_line_id.name', '=', line), ('state', 'in', ['ready', 'progress']),
('routing_type', '=', 'CNC加工')
]) ])
# print(not_done_orders)
# 完成订单 # 完成订单
# 获取当前时间并计算24小时前的时间 # 获取当前时间并计算24小时前的时间
@@ -807,16 +827,18 @@ class Sf_Dashboard_Connect(http.Controller):
'draft': '待排程', 'draft': '待排程',
'done': '已排程', 'done': '已排程',
'processing': '生产中', 'processing': '生产中',
'finished': '已完成' 'finished': '已完成',
'ready': '待加工',
'progress': '生产中',
} }
line_dict = { line_dict = {
'sequence': id_to_sequence[order.id], 'sequence': id_to_sequence[order.id],
'workorder_name': order.name, 'workorder_name': order.production_id.name,
'blank_name': blank_name, 'blank_name': blank_name,
'material': material, 'material': material,
'dimensions': dimensions, 'dimensions': dimensions,
'order_qty': order.product_qty, 'order_qty': 1,
'state': state_dict[order.state], 'state': state_dict[order.state],
} }
@@ -897,15 +919,17 @@ class Sf_Dashboard_Connect(http.Controller):
cur.execute(sql2, (item,)) cur.execute(sql2, (item,))
result2 = cur.fetchall() result2 = cur.fetchall()
# print('result2========', result2)
#
for row in result: for row in result:
res['data'][item] = {'idle_count': row[0]} res['data'][item] = {'idle_count': row[0]}
alarm_count = [] alarm_count = []
for row in result2: for row in result2:
alarm_count.append(row[1]) alarm_count.append(row[1])
if row[0]: if row[0]:
total_alarm_time += abs(float(row[0])) if float(row[0]) >= 28800:
continue
# total_alarm_time += abs(float(row[0]))
total_alarm_time += float(row[0])
else: else:
total_alarm_time += 0.0 total_alarm_time += 0.0
if len(list(set(alarm_count))) == 1: if len(list(set(alarm_count))) == 1:
@@ -915,6 +939,7 @@ class Sf_Dashboard_Connect(http.Controller):
alarm_count_num = 1 alarm_count_num = 1
else: else:
alarm_count_num = len(list(set(alarm_count))) alarm_count_num = len(list(set(alarm_count)))
res['data'][item]['total_alarm_time'] = total_alarm_time / 3600 res['data'][item]['total_alarm_time'] = total_alarm_time / 3600
res['data'][item]['alarm_count_num'] = alarm_count_num res['data'][item]['alarm_count_num'] = alarm_count_num
@@ -1332,7 +1357,7 @@ class Sf_Dashboard_Connect(http.Controller):
for result in results: for result in results:
alarm_last_24_nums.append(result[1]) alarm_last_24_nums.append(result[1])
if result[0]: if result[0]:
if float(result[0]) >= 1000: if float(result[0]) >= 28800:
continue continue
alarm_last_24_time += float(result[0]) alarm_last_24_time += float(result[0])
else: else:
@@ -1350,7 +1375,7 @@ class Sf_Dashboard_Connect(http.Controller):
for result in results: for result in results:
alarm_all_nums.append(result[1]) alarm_all_nums.append(result[1])
if result[0]: if result[0]:
if float(result[0]) >= 1000: if float(result[0]) >= 28800:
continue continue
alarm_all_time += float(result[0]) alarm_all_time += float(result[0])
else: else:
@@ -1385,3 +1410,207 @@ class Sf_Dashboard_Connect(http.Controller):
conn.close() conn.close()
return json.dumps(res) return json.dumps(res)
@http.route('/api/utilization/rate', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*")
def UtilizationRate(self, **kw):
"""
获取稼动率
"""
logging.info("kw=:%s" % kw)
res = {'status': 1, 'message': '成功', 'data': {}}
# 获取请求的机床数据
machine_list = ast.literal_eval(kw['machine_list'])
line = kw['line']
orders = request.env['mrp.workorder'].sudo().search([
('routing_type', '=', 'CNC加工'), # 将第一个条件合并进来
('production_line_id.name', '=', line),
('state', 'in', ['done'])
])
faulty_plans = request.env['quality.check'].sudo().search([
('operation_id.name', '=', 'CNC加工'),
('quality_state', 'in', ['fail'])
])
# 计算时间范围
now = datetime.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
total_power_on_time = 0
month_power_on_time = 0
today_power_on_time = 0
today_power_on_dict = {}
today_data = []
month_data = []
today_check_ng = []
month_check_ng = []
total_alarm_time = 0
today_alarm_time = 0
month_alarm_time = 0
for order in orders:
time = datetime.strptime(order.date_finished, "%Y-%m-%d %H:%M:%S")
if time >= today_start:
today_data.append(order)
if time >= month_start:
month_data.append(order)
for faulty_plan in faulty_plans:
time = faulty_plan.write_date
if time >= today_start:
today_check_ng.append(faulty_plan)
if time >= month_start:
month_check_ng.append(faulty_plan)
# 连接数据库
conn = psycopg2.connect(**db_config)
for item in machine_list:
with conn.cursor() as cur:
cur.execute("""
(
SELECT power_on_time, 'latest' AS record_type
FROM device_data
WHERE device_name = %s
AND power_on_time IS NOT NULL
ORDER BY time DESC
LIMIT 1
)
UNION ALL
(
SELECT power_on_time, 'month_first' AS record_type
FROM device_data
WHERE device_name = %s
AND power_on_time IS NOT NULL
AND time >= date_trunc('month', CURRENT_DATE) -- ✅ 修复日期函数
AND time < (date_trunc('month', CURRENT_DATE) + INTERVAL '1 month')::date
ORDER BY time ASC
LIMIT 1
)
UNION ALL
(
SELECT power_on_time, 'day_first' AS record_type
FROM device_data
WHERE device_name = %s
AND power_on_time IS NOT NULL
AND time::date = CURRENT_DATE -- ✅ 更高效的写法
ORDER BY time ASC
LIMIT 1
);
""", (item, item, item))
results = cur.fetchall()
print(results)
if len(results) >= 1:
total_power_on_time += convert_to_seconds(results[0][0])
else:
total_power_on_time += 0
if len(results) >= 2:
month_power_on_time += convert_to_seconds(results[1][0])
else:
month_power_on_time += 0
if len(results) >= 3:
today_power_on_time += convert_to_seconds(results[2][0])
today_power_on_dict[item] = today_power_on_time
else:
today_power_on_time += 0
print(total_power_on_time, month_power_on_time, today_power_on_time)
with conn.cursor() as cur:
cur.execute("""
SELECT DISTINCT ON (alarm_start_time) alarm_time, alarm_start_time
FROM device_data
WHERE device_name = %s AND alarm_start_time IS NOT NULL
ORDER BY alarm_start_time, time;
""", (item,))
results = cur.fetchall()
today_data = []
month_data = []
for record in results:
if record[0]:
if float(record[0]) >= 28800:
continue
total_alarm_time += float(record[0])
else:
total_alarm_time += 0.0
alarm_start = datetime.strptime(record[1], "%Y-%m-%d %H:%M:%S")
if alarm_start >= today_start:
today_data.append(record)
if alarm_start >= month_start:
month_data.append(record)
for today in today_data:
if today[0]:
if float(today[0]) >= 28800:
continue
today_alarm_time += float(today[0])
else:
today_alarm_time += 0.0
for month in month_data:
if month[0]:
if float(month[0]) >= 28800:
continue
month_alarm_time += float(month[0])
else:
month_alarm_time += 0.0
conn.close()
print('报警时间=============', total_alarm_time, month_alarm_time, today_alarm_time)
logging.info("报警时间=%s" % total_alarm_time)
logging.info("报警时间=%s" % month_alarm_time)
logging.info("报警时间=%s" % today_alarm_time)
# 计算时间开动率(累计、月、日)
if total_power_on_time:
total_power_on_rate = (total_power_on_time - total_alarm_time) / total_power_on_time
else:
total_power_on_rate = 0
if month_power_on_time:
month_power_on_rate = (total_power_on_time - month_power_on_time - month_alarm_time) / month_power_on_time
else:
month_power_on_rate = 0
if today_power_on_time:
today_power_on_rate = (total_power_on_time - today_power_on_time - today_alarm_time) / today_power_on_time
else:
today_power_on_rate = 0
print("总开动率: %s" % total_power_on_rate)
print("月开动率: %s" % month_power_on_rate)
print("日开动率: %s" % today_power_on_rate)
# 计算性能开动率(累计、月、日)
print('===========',orders)
print(len(orders))
total_performance_rate = len(orders) * 30 * 60 / (total_power_on_time - total_alarm_time)
month_performance_rate = len(month_data) * 30 * 60 / (month_power_on_time - month_alarm_time)
today_performance_rate = len(today_data) * 30 * 60 / (today_power_on_time - today_alarm_time) if today_power_on_time != 0 else 0
print("总性能率: %s" % total_performance_rate)
print("月性能率: %s" % month_performance_rate)
print("日性能率: %s" % today_performance_rate)
# 计算累计合格率
total_pass_rate = (len(orders) - len(today_check_ng)) / len(orders) if len(orders) != 0 else 0
month_pass_rate = (len(month_data) - len(month_check_ng)) / len(month_data) if len(month_data) != 0 else 0
today_pass_rate = (len(today_data) - len(today_check_ng)) / len(today_data) if len(today_data) != 0 else 0
print("总合格率: %s" % total_pass_rate)
print("月合格率: %s" % month_pass_rate)
print("日合格率: %s" % today_pass_rate)
# # 返回数据
# res['data'][item] = {
# 'total_utilization_rate': total_power_on_rate * total_performance_rate * total_pass_rate,
# 'month_utilization_rate': month_power_on_rate * month_performance_rate * month_pass_rate,
# 'today_utilization_rate': today_power_on_rate * today_performance_rate * today_pass_rate,
# }
res['data'] = {
'total_utilization_rate': total_power_on_rate * total_performance_rate * total_pass_rate,
'month_utilization_rate': month_power_on_rate * month_performance_rate * month_pass_rate,
'today_utilization_rate': today_power_on_rate * today_performance_rate * today_pass_rate,
}
return json.dumps(res)

View File

@@ -87,11 +87,12 @@ class AgvScheduling(models.Model):
agv_route_type: AGV任务类型 agv_route_type: AGV任务类型
workorders: 工单 workorders: 工单
""" """
scheduling = None
_logger.info('创建AGV调度任务\r\n起点为【%s】,任务类型为【%s】,工单为【%s' % (agv_start_site_name, agv_route_type, workorders)) _logger.info('创建AGV调度任务\r\n起点为【%s】,任务类型为【%s】,工单为【%s' % (agv_start_site_name, agv_route_type, workorders))
if not workorders: if not workorders:
raise UserError(_('工单不能为空')) raise UserError(_('工单不能为空'))
agv_start_site = self.env['sf.agv.site'].sudo().search([('name', '=', agv_start_site_name)], limit=1) agv_start_sites = self.env['sf.agv.site'].sudo().search([('name', '=', agv_start_site_name)])
if not agv_start_site: if not agv_start_sites:
raise UserError(_('不存在名称为【%s】的接驳站,请先创建!' % agv_start_site_name)) raise UserError(_('不存在名称为【%s】的接驳站,请先创建!' % agv_start_site_name))
# 如果存在相同任务类型工单的AGV调度任务则提示错误 # 如果存在相同任务类型工单的AGV调度任务则提示错误
agv_scheduling = self.sudo().search([ agv_scheduling = self.sudo().search([
@@ -107,24 +108,32 @@ class AgvScheduling(models.Model):
(','.join(repetitive_workorders.mapped('production_id.name')), agv_scheduling.name) (','.join(repetitive_workorders.mapped('production_id.name')), agv_scheduling.name)
) )
# 如果只有唯一任务路线,则自动赋予终点接驳站跟任务名称
agv_routes = self.env['sf.agv.task.route'].sudo().search([
('route_type', '=', agv_route_type),
('start_site_id', 'in', agv_start_sites.ids)
])
vals = { vals = {
'start_site_id': agv_start_site.id,
'agv_route_type': agv_route_type, 'agv_route_type': agv_route_type,
'workorder_ids': workorders.ids, 'workorder_ids': workorders.ids,
# 'workpiece_delivery_ids': deliveries.mapped('id') if deliveries else [], # 'workpiece_delivery_ids': deliveries.mapped('id') if deliveries else [],
'task_create_time': fields.Datetime.now() 'task_create_time': fields.Datetime.now()
} }
# 如果只有唯一任务路线,则自动赋予终点接驳站跟任务名称
agv_routes = self.env['sf.agv.task.route'].sudo().search([
('route_type', '=', agv_route_type),
('start_site_id', '=', agv_start_site.id)
])
if not agv_routes: if not agv_routes:
raise UserError(_('不存在起点为【%s】的【%s】任务路线,请先创建!' % (agv_start_site_name, agv_route_type))) raise UserError(_('不存在起点为【%s】的【%s】任务路线,请先创建!' % (agv_start_site_name, agv_route_type)))
# 如果路线中包含起点与终点相同的接驳站则不创建AGV调度任务
if agv_routes.filtered(lambda r: r.start_site_id.name == r.end_site_id.name):
return True
# 配送类型相同的接驳站为同一个,取第一个即可
vals.update({
'start_site_id': agv_routes[0].start_site_id.id,
})
idle_route = None idle_route = None
if len(agv_routes) == 1: if len(agv_routes) == 1:
idle_route = agv_routes[0] idle_route = agv_routes[0]
vals.update({'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id}) vals.update({
'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id
})
else: else:
# 判断终点接驳站是否为空闲 # 判断终点接驳站是否为空闲
idle_routes = agv_routes.filtered(lambda r: r.end_site_id.state == '空闲') idle_routes = agv_routes.filtered(lambda r: r.end_site_id.state == '空闲')
@@ -132,7 +141,10 @@ class AgvScheduling(models.Model):
# 将空闲的路线按照终点接驳站名称排序 # 将空闲的路线按照终点接驳站名称排序
idle_routes = sorted(idle_routes, key=lambda r: r.end_site_id.name) idle_routes = sorted(idle_routes, key=lambda r: r.end_site_id.name)
idle_route = idle_routes[0] idle_route = idle_routes[0]
vals.update({'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id}) vals.update({
'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id
})
try: try:
scheduling = self.env['sf.agv.scheduling'].sudo().create(vals) scheduling = self.env['sf.agv.scheduling'].sudo().create(vals)
# 触发空闲接驳站状态更新,触发新任务下发 # 触发空闲接驳站状态更新,触发新任务下发
@@ -142,7 +154,7 @@ class AgvScheduling(models.Model):
except Exception as e: except Exception as e:
_logger.error('添加AGV调度任务失败: %s', e) _logger.error('添加AGV调度任务失败: %s', e)
raise UserError(_('添加AGV调度任务失败: %s', e)) raise UserError(_('添加AGV调度任务失败: %s', e))
return scheduling return scheduling
def on_site_state_change(self, agv_site_id, agv_site_state): def on_site_state_change(self, agv_site_id, agv_site_state):

View File

@@ -24,7 +24,7 @@ class AgvSetting(models.Model):
# name必须唯一 # name必须唯一
_sql_constraints = [ _sql_constraints = [
('name_uniq', 'unique (name)', '站点编号必须唯一!'), ('name_uniq', 'unique (name, workcenter_id)', '同一工作中心的站点编号必须唯一!'),
] ]
# def update_site_state(self): # def update_site_state(self):
@@ -68,11 +68,12 @@ class AgvSetting(models.Model):
""" """
if isinstance(agv_site_state_arr, dict): if isinstance(agv_site_state_arr, dict):
for agv_site_name, is_occupy in agv_site_state_arr.items(): 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)]) agv_sites = self.env['sf.agv.site'].sudo().search([('name', '=', agv_site_name)])
if agv_site: if agv_sites:
agv_site.state = is_occupy agv_sites.state = is_occupy
if notify: if notify:
self.env['sf.agv.scheduling'].on_site_state_change(agv_site.id, agv_site.state) for agv_site in agv_sites:
self.env['sf.agv.scheduling'].on_site_state_change(agv_site.id, agv_site.state)
else: else:
_logger.error("更新失败:接驳站站点错误!%s" % agv_site_name) _logger.error("更新失败:接驳站站点错误!%s" % agv_site_name)
raise UserError("更新失败:接驳站站点错误!") raise UserError("更新失败:接驳站站点错误!")

View File

@@ -15,6 +15,23 @@
</field> </field>
</record> </record>
<record id="view_agv_site_form" model="ir.ui.view">
<field name="name">agv.site.form</field>
<field name="model">sf.agv.site</field>
<field name="arch" type="xml">
<form create="false" edit="false">
<sheet>
<group>
<field name="name" readonly="1" required="1"/>
<field name="workcenter_id" readonly="1" required="1" options="{'no_create': True}"/>
<field name="state" readonly="1" required="1"/>
<field name="divide_the_work" readonly="1" required="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_agv_site_form" model="ir.actions.act_window"> <record id="action_agv_site_form" model="ir.actions.act_window">
<field name="name">AGV站点</field> <field name="name">AGV站点</field>
<field name="res_model">sf.agv.site</field> <field name="res_model">sf.agv.site</field>
@@ -39,7 +56,8 @@
<field name="route_type" string="类型" required="1" attrs="{'readonly': [('id', '!=', False)]}"/> <field name="route_type" string="类型" required="1" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="start_site_id" required="1" options="{'no_create': True}" string="起点接驳站" <field name="start_site_id" required="1" options="{'no_create': True}" string="起点接驳站"
attrs="{'readonly': [('id', '!=', False)]}"/> attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="end_site_id" required="1" options="{'no_create': True}" string="终点接驳站"/> <field name="end_site_id" required="1" options="{'no_create': True}" string="终点接驳站"
attrs="{'readonly': [('id', '!=', False)]}"/>
<!-- <field name="destination_production_line_id" required="1" options="{'no_create': True}"--> <!-- <field name="destination_production_line_id" required="1" options="{'no_create': True}"-->
<!-- attrs="{'readonly': [('id', '!=', False)]}"/>--> <!-- attrs="{'readonly': [('id', '!=', False)]}"/>-->
<field name="workcenter_id"/> <field name="workcenter_id"/>

View File

@@ -117,17 +117,30 @@ class WorkpieceDeliveryWizard(models.TransientModel):
item.button_finish() item.button_finish()
# return scheduling.read()[0] # return scheduling.read()[0]
return { if isinstance(scheduling, bool) and scheduling is True:
'type': 'ir.actions.client', return{
'tag': 'display_notification', 'type': 'ir.actions.client',
'target': 'new', 'tag': 'display_notification',
'params': { 'target': 'new',
'message': f'任务下发成功AGV任务调度编号为【{scheduling.name}', 'params': {
'type': 'success', 'message': f'解除装夹成功',
'sticky': False, 'type': 'success',
'next': {'type': 'ir.actions.act_window_close'}, 'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
}
}
else:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'target': 'new',
'params': {
'message': f'任务下发成功AGV任务调度编号为【{scheduling.name}',
'type': 'success',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
}
} }
}
except Exception as e: except Exception as e:
logging.info('%s任务下发失败:%s', self.delivery_type, e) logging.info('%s任务下发失败:%s', self.delivery_type, e)
raise UserError(f'{self.delivery_type}任务下发失败:{e}') from e raise UserError(f'{self.delivery_type}任务下发失败:{e}') from e

View File

@@ -61,6 +61,29 @@
</div> </div>
</div> </div>
</template> </template>
<!-- 定义页脚模板无页码 -->
<template id="html_report_quality_footer">
<div class="footer">
<div style="border-top: 3px solid black;"></div>
<div class="row">
<div class="col-6">
<p>售后服务: <span t-field="o.company_id.phone"/></p>
<p>公司名称: <span t-field="o.company_id.name"/></p>
<p>加工工厂: <span t-field="o.company_id.factory_name"/></p>
</div>
<div class="col-6">
<p>公司网址: <span t-field="o.company_id.website"/></p>
<p>公司邮箱: <span t-field="o.company_id.email"/></p>
</div>
</div>
<!-- <div style="border-top: 2px solid black;"></div> -->
<div class="text-center">
<span><span>1</span> 页/共 <span>1</span></span>
</div>
</div>
</template>
<template id="report_quality_inspection"> <template id="report_quality_inspection">
<t t-call="web.html_container"> <t t-call="web.html_container">
<t t-foreach="docs" t-as="o"> <t t-foreach="docs" t-as="o">
@@ -73,7 +96,7 @@
<table class="table table-sm o_main_table mt-4" style="border: 1px solid black;"> <table class="table table-sm o_main_table mt-4" style="border: 1px solid black;">
<tr> <tr>
<td style="width: 15%; border: 1px solid black;"><strong>产品名称:</strong></td> <td style="width: 15%; border: 1px solid black;"><strong>产品名称:</strong></td>
<td style="width: 35%; border: 1px solid black;"><span t-field="o.product_id.name"/></td> <td style="width: 35%; border: 1px solid black;"><span t-field="o.part_name"/></td>
<td style="width: 15%; border: 1px solid black;"><strong>材料:</strong></td> <td style="width: 15%; border: 1px solid black;"><strong>材料:</strong></td>
<td style="width: 35%; border: 1px solid black;"><span t-field="o.material_name"/></td> <td style="width: 35%; border: 1px solid black;"><span t-field="o.material_name"/></td>
</tr> </tr>
@@ -187,4 +210,131 @@
</t> </t>
</t> </t>
</template> </template>
<template id="html_report_quality_inspection">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.basic_layout">
<t t-call="sf_quality.report_quality_header"/>
<div class="page" style="min-height: 800px; position: relative; padding-bottom: 250px;">
<table class="table table-sm o_main_table mt-4" style="border: 1px solid black;">
<tr>
<td style="width: 15%; border: 1px solid black;"><strong>产品名称:</strong></td>
<td style="width: 35%; border: 1px solid black;"><span t-field="o.part_name"/></td>
<td style="width: 15%; border: 1px solid black;"><strong>材料:</strong></td>
<td style="width: 35%; border: 1px solid black;"><span t-field="o.material_name"/></td>
</tr>
<tr>
<td style="border: 1px solid black;"><strong>图号:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.part_number"/></td>
<td style="border: 1px solid black;"><strong>日期:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.write_date"/></td>
</tr>
<tr>
<td style="border: 1px solid black;"><strong>总数量:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.total_qty"/></td>
<td style="border: 1px solid black;"><strong>检验数量:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.check_qty"/></td>
</tr>
</table>
<h4 class="text-center mt-4">检验结果</h4>
<div class="" style="position: relative;">
<table class="table table-sm mt-2" style="border: 1px solid black;">
<thead>
<tr>
<th style="border: 1px solid black;" class="text-center" rowspan="2">检测项目<br/>(图示尺寸)</th>
<th style="border: 1px solid black;" t-att-colspan="o.column_nums" class="text-center">测量值</th>
<th style="border: 1px solid black; vertical-align: middle;" class="text-center" rowspan="2">判定</th>
<th style="border: 1px solid black; vertical-align: middle;" class="text-center" rowspan="2">备注</th>
</tr>
<tr>
<!-- <th style="border: 1px solid black;"></th> -->
<th style="border: 1px solid black;" t-if="o.column_nums >= 1" class="text-center">1</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 2" class="text-center">2</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 3" class="text-center">3</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 4" class="text-center">4</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 5" class="text-center">5</th>
<!-- <th style="border: 1px solid black;"></th>
<th style="border: 1px solid black;"></th> -->
</tr>
</thead>
<tbody>
<tr t-foreach="o.measure_line_ids" t-as="line">
<td style="border: 1px solid black;" class="text-center"><span t-field="line.measure_item"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 1" class="text-center"><span t-field="line.measure_value1"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 2" class="text-center"><span t-field="line.measure_value2"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 3" class="text-center"><span t-field="line.measure_value3"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 4" class="text-center"><span t-field="line.measure_value4"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 5" class="text-center"><span t-field="line.measure_value5"/></td>
<td style="border: 1px solid black;" class="text-center"><span t-field="line.measure_result"/></td>
<td style="border: 1px solid black;" class="text-center"><span t-field="line.remark"/></td>
</tr>
</tbody>
</table>
<img src="/sf_quality/static/img/pass.png" style="width: 200px; height: 200px;position: absolute; bottom: 20px; right: 20%;"/>
</div>
<div style="clear: both; margin-top: 30px; padding-top: 10px;">
<div style="display: inline-block;">
<span style="font-size: 18px; font-weight: bold;">检验结论: </span>
<span t-if="o.report_result == 'OK'" style="margin-left: 30px; display: inline-block;">
<svg width="20" height="20" style="vertical-align: middle;">
<rect x="1" y="1" width="18" height="18" fill="none" stroke="black" stroke-width="1.5"/>
<path d="M4 10 L9 15 L16 6" stroke="black" stroke-width="2" fill="none"/>
</svg>
<span style="margin-left: 5px;">合格</span>
</span>
<span t-else="" style="margin-left: 30px; display: inline-block;">
<svg width="20" height="20" style="vertical-align: middle;">
<rect x="1" y="1" width="18" height="18" fill="none" stroke="black" stroke-width="1.5"/>
</svg>
<span style="margin-left: 5px;">合格</span>
</span>
<span t-if="o.report_result == 'NG'" style="margin-left: 50px; display: inline-block;">
<svg width="20" height="20" style="vertical-align: middle;">
<rect x="1" y="1" width="18" height="18" fill="none" stroke="black" stroke-width="1.5"/>
<path d="M4 10 L9 15 L16 6" stroke="black" stroke-width="2" fill="none"/>
</svg>
<span style="margin-left: 5px;">不合格</span>
</span>
<span t-else="" style="margin-left: 50px; display: inline-block;">
<svg width="20" height="20" style="vertical-align: middle;">
<rect x="1" y="1" width="18" height="18" fill="none" stroke="black" stroke-width="1.5"/>
</svg>
<span style="margin-left: 5px;">不合格</span>
</span>
</div>
</div>
<div class="row mt-4">
<div class="col-6">
<p><strong>操作员: </strong> <span t-field="o.measure_operator"/></p>
</div>
<div class="col-6">
<p><strong>质检员: </strong> <span t-field="o.quality_manager"/></p>
</div>
</div>
<div style="border-top: 3px solid black;"></div>
<!-- 添加合格标签 -->
<!-- <div class="row mt-5">
<div class="col-12 text-center">
<p></p>
</div>
</div> -->
<!-- 页脚固定在底部 -->
<!-- <div style="position: absolute; bottom: 0; left: 0; right: 0;"> -->
<t t-call="sf_quality.html_report_quality_footer"/>
<!-- </div> -->
</div>
</t>
</t>
</t>
</template>
</odoo> </odoo>

View File

@@ -18,7 +18,7 @@
<field name="name">预览检验报告</field> <field name="name">预览检验报告</field>
<field name="model">quality.check</field> <field name="model">quality.check</field>
<field name="report_type">qweb-html</field> <field name="report_type">qweb-html</field>
<field name="report_name">sf_quality.report_quality_inspection</field> <field name="report_name">sf_quality.html_report_quality_inspection</field>
<field name="binding_type">report</field> <field name="binding_type">report</field>
</record> </record>
</odoo> </odoo>

View File

@@ -38,6 +38,7 @@
'web.assets_qweb': [ 'web.assets_qweb': [
], ],
'web.assets_backend': [ 'web.assets_backend': [
'sf_tool_management/static/src/change.scss'
] ]
}, },

View File

@@ -387,7 +387,7 @@ class FunctionalToolAssembly(models.Model):
else: else:
raise ValidationError('刀柄选择错误,请重新确认!!!') raise ValidationError('刀柄选择错误,请重新确认!!!')
else: else:
location = self.env['sf.shelf.location'].sudo().search([('barcode', '=', barcode)]) location = self.env['sf.shelf.location'].sudo().search([('barcode', '=', barcode.upper())])
if location: if location:
if location == record.integral_freight_barcode_id: if location == record.integral_freight_barcode_id:
tool_assembly_id.integral_verify = True tool_assembly_id.integral_verify = True
@@ -782,10 +782,11 @@ class FunctionalToolAssembly(models.Model):
"""根据BOM对刀具物料进行初始配置""" """根据BOM对刀具物料进行初始配置"""
options = bom.get('options') options = bom.get('options')
# 配置刀柄信息 # 配置刀柄信息
for handle_id in bom.get('handle_ids'): handle_ids = self._get_old_tool_material_lot(bom.get('handle_ids'))
for handle_id in handle_ids:
if handle_id: if handle_id:
if not self.handle_product_id: if not self.handle_product_id:
self.handle_product_id = handle_id.id self.handle_product_id = handle_id.product_id.id
break break
# 刀柄之外的物料配置 # 刀柄之外的物料配置
@@ -820,19 +821,20 @@ class FunctionalToolAssembly(models.Model):
location_id = self.env['stock.location'].search([('name', '=', '刀具房')]) location_id = self.env['stock.location'].search([('name', '=', '刀具房')])
stock_quant = self.env['stock.quant'].sudo().search( stock_quant = self.env['stock.quant'].sudo().search(
[('location_id', '=', location_id.id), ('product_id', 'in', material_ids.ids), ('quantity', '>', '0')], [('location_id', '=', location_id.id), ('product_id', 'in', material_ids.ids), ('quantity', '>', '0')],
order='lot_id', limit=1) order='lot_id')
if stock_quant: if stock_quant:
return stock_quant.lot_id return [quant.lot_id for quant in stock_quant]
else: else:
raise ValidationError(f'{material_ids[0].cutting_tool_material_id.name}】物料库存不足,请先进行盘点或采购') raise ValidationError(f'{material_ids[0].cutting_tool_material_id.name}】物料库存不足,请先进行盘点或采购')
def _get_shelf_location_lot(self, lot_id): def _get_shelf_location_lot(self, lot_ids):
"""根据所给的刀具物料批次号,返回一个刀具物料货位、批次信息""" """根据所给的刀具物料批次号,返回一个刀具物料货位、批次信息"""
location_lots = self.env['sf.shelf.location.lot'].sudo().search([('lot_id', '=', lot_id.id)]) for lot_id in lot_ids:
if not location_lots: location_lots = self.env['sf.shelf.location.lot'].sudo().search([('lot_id', '=', lot_id.id)])
raise ValidationError(f'没有查询到批次为【{lot_id.name}】物料的货位信息!') if location_lots:
else: return location_lots[0]
return location_lots[0] raise ValidationError(f'{lot_ids[0].product_id.cutting_tool_material_id.name}】物料在货位库存不足,请先进行盘点入库')
def _get_inventory_bom(self, inventory_id): def _get_inventory_bom(self, inventory_id):
"""获取BOM的刀具物料产品信息""" """获取BOM的刀具物料产品信息"""
@@ -1240,7 +1242,7 @@ class FunctionalToolDismantle(models.Model):
functional_tool_id = fields.Many2one('sf.functional.cutting.tool.entity', '功能刀具', required=True, tracking=True, functional_tool_id = fields.Many2one('sf.functional.cutting.tool.entity', '功能刀具', required=True, tracking=True,
domain=[('functional_tool_status', '!=', '已拆除'), domain=[('functional_tool_status', '!=', '已拆除'),
('current_location', '=', '刀具房')]) ('current_location', 'in', ['刀具房', '线边刀库'])])
@api.onchange('functional_tool_id') @api.onchange('functional_tool_id')
def _onchange_functional_tool_id(self): def _onchange_functional_tool_id(self):
@@ -1437,14 +1439,26 @@ class FunctionalToolDismantle(models.Model):
def confirmation_disassembly(self): def confirmation_disassembly(self):
logging.info('%s刀具确认开始拆解' % self.dismantle_cause) logging.info('%s刀具确认开始拆解' % self.dismantle_cause)
code = self.code code = self.code
context = self.env.context
if self.functional_tool_id.functional_tool_status == '已拆除': if self.functional_tool_id.functional_tool_status == '已拆除':
raise ValidationError('Rfid为【%s】名称为【%s】的功能刀具已经拆解,请勿重复操作!' % ( raise ValidationError('Rfid为【%s】名称为【%s】的功能刀具已经拆解,请勿重复操作!' % (
self.functional_tool_id.rfid_dismantle, self.name)) self.functional_tool_id.rfid_dismantle, self.name))
# 对拆解的功能刀具进行校验,只有在刀具房的功能刀具才能拆解 # 对拆解的功能刀具进行校验,只有在刀具房的功能刀具才能拆解
elif self.functional_tool_id.functional_tool_status != '报警': elif self.functional_tool_id.functional_tool_status != '报警':
if self.functional_tool_id.tool_room_num == 0: if self.functional_tool_id.current_location == '机内刀库':
raise ValidationError('Rfid为【%s】的功能刀具当前位置为【%s】,不能进行拆解!' % ( raise ValidationError('Rfid为【%s】的功能刀具当前位置为【%s】,不能进行拆解!' % (
self.rfid, self.functional_tool_id.current_location)) self.rfid, self.functional_tool_id.current_location))
elif not context.get('TRUE_DISASSEMBLE') and self.functional_tool_id.current_location == '线边刀库':
return {
'type': 'ir.actions.act_window',
'res_model': 'sf.functional.tool.dismantle.wiard',
'name': '刀具寿命未到期',
'view_mode': 'form',
'target': 'new',
'context': {
'default_functional_tool_dismantle_id': self.id,
'TRUE_DISASSEMBLE': True}
}
# 目标重复校验 # 目标重复校验
self.location_duplicate_check() self.location_duplicate_check()
datas = {'scrap': [], 'picking': []} datas = {'scrap': [], 'picking': []}
@@ -1521,11 +1535,18 @@ class FunctionalToolDismantle(models.Model):
'functional_tool_name': self.functional_tool_id.name, 'functional_tool_name': self.functional_tool_id.name,
'handle_code_id': self.handle_lot_id.id, 'handle_code_id': self.handle_lot_id.id,
'handle_product_id': self.handle_product_id.id, 'handle_product_id': self.handle_product_id.id,
'functional_tool_diameter': self.functional_tool_id.functional_tool_diameter,
'knife_tip_r_angle': self.functional_tool_id.knife_tip_r_angle,
'tool_loading_length': self.functional_tool_id.tool_loading_length,
'functional_tool_length': self.functional_tool_id.functional_tool_length,
'loading_task_source': '3', 'loading_task_source': '3',
'use_tool_time': fields.Datetime.now() + timedelta(hours=4), 'use_tool_time': fields.Datetime.now() + timedelta(hours=4),
'reason_for_applying': '刀具寿命到期' 'reason_for_applying': '刀具寿命到期'
}) })
# 将新的组装单更新到对应的功能刀具安全库存的组装单列表中
self.functional_tool_id.safe_inventory_id.sudo().sf_functional_tool_assembly_ids = [(4, assembly_id.id)]
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'res_model': 'sf.functional.tool.assembly', 'res_model': 'sf.functional.tool.assembly',

View File

@@ -10,6 +10,7 @@ from odoo.exceptions import ValidationError
class FunctionalCuttingToolEntity(models.Model): class FunctionalCuttingToolEntity(models.Model):
_name = 'sf.functional.cutting.tool.entity' _name = 'sf.functional.cutting.tool.entity'
_inherit = ['mail.thread']
_description = '功能刀具列表' _description = '功能刀具列表'
_order = 'functional_tool_status' _order = 'functional_tool_status'
@@ -41,7 +42,7 @@ class FunctionalCuttingToolEntity(models.Model):
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)
functional_tool_status = fields.Selection([('正常', '正常'), ('报警', '报警'), ('已拆除', '已拆除')], functional_tool_status = fields.Selection([('正常', '正常'), ('报警', '报警'), ('已拆除', '已拆除')], tracking=True,
string='状态', store=True, default='正常') string='状态', store=True, default='正常')
current_location_id = fields.Many2one('stock.location', string='当前位置', compute='_compute_current_location_id', current_location_id = fields.Many2one('stock.location', string='当前位置', compute='_compute_current_location_id',
store=True) store=True)
@@ -62,16 +63,27 @@ class FunctionalCuttingToolEntity(models.Model):
for item in self: for item in self:
if item: if item:
if item.functional_tool_status == '报警': if item.functional_tool_status == '报警':
# 创建报警刀具拆解单 self.create_tool_dismantle()
self.env['sf.functional.tool.dismantle'].sudo().create({
'functional_tool_id': item.ids[0], def set_functional_tool_status(self):
'dismantle_cause': '寿命到期报废' # self.write({
}) # 'functional_tool_status': '报警'
# 创建刀具报警记录 # })
self.env['sf.functional.tool.warning'].sudo().create({ self.functional_tool_status = '报警'
'rfid': item.rfid, self.create_tool_dismantle()
'functional_tool_id': item.ids[0]
}) def create_tool_dismantle(self):
for item in self:
# 创建报警刀具拆解单
self.env['sf.functional.tool.dismantle'].sudo().create({
'functional_tool_id': item.ids[0],
'dismantle_cause': '寿命到期报废'
})
# 创建刀具报警记录
self.env['sf.functional.tool.warning'].sudo().create({
'rfid': item.rfid,
'functional_tool_id': item.ids[0]
})
@api.depends('barcode_id.quant_ids', 'barcode_id.quant_ids.location_id', 'functional_tool_status', @api.depends('barcode_id.quant_ids', 'barcode_id.quant_ids.location_id', 'functional_tool_status',
'current_shelf_location_id', 'stock_num') 'current_shelf_location_id', 'stock_num')
@@ -263,7 +275,7 @@ class FunctionalCuttingToolEntity(models.Model):
functional_tool_model_ids.append(functional_tool_model.id) functional_tool_model_ids.append(functional_tool_model.id)
return [(6, 0, functional_tool_model_ids)] return [(6, 0, functional_tool_model_ids)]
dismantle_num = fields.Integer('拆解单数量', compute='_compute_dismantle_num', store=True) dismantle_num = fields.Integer('拆解单数量', compute='_compute_dismantle_num', tracking=True, store=True)
dismantle_ids = fields.One2many('sf.functional.tool.dismantle', 'functional_tool_id', '拆解单') dismantle_ids = fields.One2many('sf.functional.tool.dismantle', 'functional_tool_id', '拆解单')
@api.depends('dismantle_ids') @api.depends('dismantle_ids')

View File

@@ -107,11 +107,17 @@ class SfMaintenanceEquipment(models.Model):
if functional_tool_id.current_location != '机内刀库': if functional_tool_id.current_location != '机内刀库':
# 对功能刀具进行移动到生产线 # 对功能刀具进行移动到生产线
functional_tool_id.tool_inventory_displacement_out() functional_tool_id.tool_inventory_displacement_out()
functional_tool_id.write({ data_tool = {
'max_lifetime_value': data['MaxLife'], 'max_lifetime_value': data['MaxLife'],
'used_value': data['UseLife'], 'used_value': data['UseLife'],
'functional_tool_status': tool_install_time.get(data['State']) 'functional_tool_status': tool_install_time.get(data['State'])
}) }
if (functional_tool_id.functional_tool_status != '报警'
and tool_install_time.get(data['State']) == '报警'):
functional_tool_id.write(data_tool)
functional_tool_id.create_tool_dismantle()
else:
functional_tool_id.write(data_tool)
else: else:
logging.info('获取的【%s】设备不存在!!!' % data['DeviceId']) logging.info('获取的【%s】设备不存在!!!' % data['DeviceId'])
else: else:

View File

@@ -40,4 +40,7 @@ access_sf_functional_tool_dismantle_group_sf_tool_user,sf.functional.tool.disman
access_sf_functional_tool_dismantle_group_plan_dispatch,sf.functional.tool.dismantle_group_plan_dispatch,model_sf_functional_tool_dismantle,sf_base.group_plan_dispatch,1,0,0,0 access_sf_functional_tool_dismantle_group_plan_dispatch,sf.functional.tool.dismantle_group_plan_dispatch,model_sf_functional_tool_dismantle,sf_base.group_plan_dispatch,1,0,0,0
access_jikimo_bom,jikimo.bom,model_jikimo_bom,base.group_user,1,1,1,1 access_jikimo_bom,jikimo.bom,model_jikimo_bom,base.group_user,1,1,1,1
access_jikimo_bom_wizard,jikimo.bom.wizard,model_jikimo_bom_wizard,base.group_user,1,1,1,1 access_jikimo_bom_wizard,jikimo.bom.wizard,model_jikimo_bom_wizard,base.group_user,1,1,1,1
access_sf_functional_tool_dismantle_wiard,sf.functional.tool.dismantle.wiard,model_sf_functional_tool_dismantle_wiard,sf_base.group_sf_tool_user,1,1,1,0
access_sf_functional_tool_dismantle_wiard_group_plan_dispatch,sf.functional.tool.dismantle.wiard,model_sf_functional_tool_dismantle_wiard,sf_base.group_plan_dispatch,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
40
41
42
43
44
45
46

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

@@ -1,17 +1,47 @@
.modal-content .o_cp_buttons { // .modal-content .o_cp_buttons {
display:none // display:none
} // }
.modal-content .o_control_panel { // .modal-content .o_control_panel {
display:none // display:none
} // }
.modal-content .o_list_button { // .modal-content .o_list_button {
// }
// .o_form_view .o_field_widget .o_list_renderer {
// width: 100%!important;
// margin:0 auto;
// overflow: auto;
// }
.o_field_widget.o_readonly_modifier.o_field_char.text-success[name=handle_freight_rfid],
.o_field_widget.o_readonly_modifier.o_field_many2one.text-success[name=integral_freight_barcode_id] {
a.text-success{
span {
color: #999;
}
}
} }
.custom_group:has(.text-success){
.o_form_view .o_field_widget .o_list_renderer { position: relative;
width: 100%!important; &::after{
margin:0 auto; content: '';
overflow: auto; display: block;
width: 72px;
height: 72px;
background: url('/sf_tool_management/static/images/replaceIcon.png') no-repeat center center;
background-size: 100%;
position: absolute;
bottom: 20px;
left: 300px;
}
} }
.o_field_widget.o_readonly_modifier.o_field_char.text-success[name=handle_freight_rfid] {
display: flex;
align-items: center;
> span {
color: #999;
}
}

View File

@@ -42,6 +42,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form create="0" edit="0" delete="0"> <form create="0" edit="0" delete="0">
<header> <header>
<button name="set_functional_tool_status" string="报警" type="object" invisible="1"/>
<!-- <button name="enroll_functional_tool_entity" string="功能刀具注册" type="object"--> <!-- <button name="enroll_functional_tool_entity" string="功能刀具注册" type="object"-->
<!-- class="btn-primary"/>--> <!-- class="btn-primary"/>-->
<field name="functional_tool_status" widget="statusbar" statusbar_visible="正常,报警,已拆除"/> <field name="functional_tool_status" widget="statusbar" statusbar_visible="正常,报警,已拆除"/>
@@ -192,6 +193,10 @@
</page> </page>
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form> </form>
</field> </field>
</record> </record>

View File

@@ -531,10 +531,10 @@
<div> <div>
<separator string="刀柄:" style="font-size: 13px;"/> <separator string="刀柄:" style="font-size: 13px;"/>
</div> </div>
<group> <group class="custom_group" >
<field name="handle_code_id" string="序列号" placeholder="请选择" <field name="handle_code_id" string="序列号" placeholder="请选择"
options="{'no_create': True, 'no_quick_create': True}"/> options="{'no_create': True, 'no_quick_create': True}"/>
<field name="handle_freight_rfid" string="Rfid"/> <field name="handle_freight_rfid" string="Rfid" decoration-success="handle_freight_rfid"/>
<field name="handle_product_id" string="名称"/> <field name="handle_product_id" string="名称"/>
<field name="cutting_tool_cutterhandle_model_id" string="型号"/> <field name="cutting_tool_cutterhandle_model_id" string="型号"/>
<field name="handle_specification_id" string="规格"/> <field name="handle_specification_id" string="规格"/>
@@ -554,15 +554,15 @@
<separator string="整体式刀具:" style="font-size: 13px;"/> <separator string="整体式刀具:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="integral_freight_barcode_id" string="货位"/> <field name="integral_freight_barcode_id" string="货位" decoration-success="integral_verify == True"/>
<field name="integral_lot_id" string="批次"/> <field name="integral_lot_id" string="批次"/>
<field name="integral_product_id" string="名称"/> <field name="integral_product_id" string="名称"/>
<field name="cutting_tool_integral_model_id" string="型号"/> <field name="cutting_tool_integral_model_id" string="型号"/>
<field name="integral_specification_id" string="规格"/> <field name="integral_specification_id" string="规格"/>
<field name="sf_tool_brand_id_1" string="品牌"/> <field name="sf_tool_brand_id_1" string="品牌"/>
</group> </group>
<group> <group invisible="1">
<field name="integral_verify" string="" readonly="1"/> <field name="integral_verify" string="" readonly="1"/>
</group> </group>
</group> </group>
@@ -582,8 +582,8 @@
<separator string="刀片:" style="font-size: 13px;"/> <separator string="刀片:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="blade_freight_barcode_id" string="货位"/> <field name="blade_freight_barcode_id" string="货位" decoration-success="blade_verify == True"/>
<field name="blade_lot_id" string="批次"/> <field name="blade_lot_id" string="批次"/>
<field name="blade_product_id" string="名称"/> <field name="blade_product_id" string="名称"/>
<field name="cutting_tool_blade_model_id" string="型号"/> <field name="cutting_tool_blade_model_id" string="型号"/>
@@ -607,8 +607,8 @@
<separator string="刀杆:" style="font-size: 13px;"/> <separator string="刀杆:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="bar_freight_barcode_id" string="货位"/> <field name="bar_freight_barcode_id" string="货位" decoration-success="bar_verify == True"/>
<field name="bar_lot_id" string="批次"/> <field name="bar_lot_id" string="批次"/>
<field name="bar_product_id" string="名称"/> <field name="bar_product_id" string="名称"/>
<field name="cutting_tool_cutterbar_model_id" string="型号"/> <field name="cutting_tool_cutterbar_model_id" string="型号"/>
@@ -631,8 +631,8 @@
<separator string="刀盘:" style="font-size: 13px;"/> <separator string="刀盘:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="pad_freight_barcode_id" string="货位"/> <field name="pad_freight_barcode_id" string="货位" decoration-success="pad_verify == True"/>
<field name="pad_lot_id" string="批次"/> <field name="pad_lot_id" string="批次"/>
<field name="pad_product_id" string="名称"/> <field name="pad_product_id" string="名称"/>
<field name="cutting_tool_cutterpad_model_id" string="型号"/> <field name="cutting_tool_cutterpad_model_id" string="型号"/>

View File

@@ -770,3 +770,12 @@ class FunctionalToolAssemblyOrder(models.TransientModel):
# } # }
class FunctionalToolDismantle(models.TransientModel):
_name = 'sf.functional.tool.dismantle.wiard'
_description = '功能刀具拆解二次确认'
functional_tool_dismantle_id = fields.Many2one('sf.functional.tool.dismantle', '拆解单')
def confirm(self):
self.functional_tool_dismantle_id.confirmation_disassembly()
return True

View File

@@ -444,4 +444,19 @@
<field name="view_id" ref="sf_functional_tool_assembly_order_form"/> <field name="view_id" ref="sf_functional_tool_assembly_order_form"/>
<field name="target">new</field> <field name="target">new</field>
</record> </record>
<record id="sf_functional_tool_dismantle_wiard_form" model="ir.ui.view">
<field name="name">刀具拆解</field>
<field name="model">sf.functional.tool.dismantle.wiard</field>
<field name="arch" type="xml">
<form>
<div>刀具寿命未到期,是否继续拆解?</div>
<footer>
<button string="确定" name="confirm" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo> </odoo>

View File

@@ -150,4 +150,4 @@ access_sf_shelf_lot_group_user,sf.shelf.location.lot.group_user,model_sf_shelf_l
access_ir_model_group_sf_stock_user,ir_model_group_sf_stock_user,base.model_ir_model,sf_base.group_sf_stock_user,1,1,0,0 access_ir_model_group_sf_stock_user,ir_model_group_sf_stock_user,base.model_ir_model,sf_base.group_sf_stock_user,1,1,0,0
access_mrp_workorder_group_sf_stock_user,mrp_workorder_group_sf_stock_user,mrp.model_mrp_workorder,sf_base.group_sf_stock_user,1,0,0,0 access_mrp_workorder_group_sf_stock_user,mrp_workorder_group_sf_stock_user,mrp.model_mrp_workorder,sf_base.group_sf_stock_user,1,1,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
150
151
152
153

View File

@@ -6,10 +6,7 @@ import win32gui
import win32con import win32con
import logging import logging
import time import time
import re
# 配置日志记录
logging.basicConfig(filename='service.log', level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
app = FastAPI() app = FastAPI()
@@ -19,11 +16,11 @@ class FileUploadRequest(BaseModel):
# FTP 服务器配置信息 # FTP 服务器配置信息
ftp_host = '110.52.114.162' ftp_host = '47.119.33.43'
ftp_port = 10021 ftp_port = 21
ftp_user = 'ftpuser' ftp_user = 'ftpuser'
ftp_password = '123456' ftp_password = 'FTPftp123'
ftp_directory = '/home/ftp/ftp_root/ThreeTest/XT/Before/' ftp_directory = 'ThreeTest/XT/Before/'
def find_child_window(parent_hwnd, class_name): def find_child_window(parent_hwnd, class_name):
@@ -63,25 +60,95 @@ def find_child_window_by_title(parent_hwnd, title):
return child_hwnds return child_hwnds
# 获取 ComboBox 的句柄
def get_combobox_handle(parent_handle):
combo_handle = win32gui.FindWindowEx(parent_handle, 0, "ComboBox", None)
if combo_handle == 0:
raise Exception("ComboBox not found")
return combo_handle
# 获取 ComboBox 中的所有选项
def get_combobox_items(combo_handle):
count = win32gui.SendMessage(combo_handle, win32con.CB_GETCOUNT, 0, 0)
items = []
for i in range(count):
length = win32gui.SendMessage(combo_handle, win32con.CB_GETLBTEXTLEN, i, 0)
buffer = win32gui.PyMakeBuffer(length + 1)
win32gui.SendMessage(combo_handle, win32con.CB_GETLBTEXT, i, buffer)
byte_data = buffer.tobytes()
# 尝试多种编码方式
text = None
# 尝试多种编码方式包括utf-16le
for encoding in ['utf-16le', 'utf-8', 'gbk', 'latin-1']:
try:
decoded_text = byte_data.decode(encoding).rstrip('\x00')
# 如果解码后的文本看起来是合理的,就接受它
if any(char.isprintable() for char in decoded_text):
text = decoded_text
break
except UnicodeDecodeError:
continue
# 如果所有解码方式都失败,或者内容仍有大量乱码,显示为十六进制字符串
if text is None or not all(char.isprintable() or char.isspace() for char in text):
text = byte_data.hex()
items.append(text)
return items
# 获取当前选定项
def get_combobox_selected(combo_handle):
index = win32gui.SendMessage(combo_handle, win32con.CB_GETCURSEL, 0, 0)
if index == -1:
return None
length = win32gui.SendMessage(combo_handle, win32con.CB_GETLBTEXTLEN, index, 0)
buffer = win32gui.PyMakeBuffer(1024) # 调整缓冲区大小
win32gui.SendMessage(combo_handle, win32con.CB_GETLBTEXT, index, buffer)
# 尝试多种编码方式进行解码
for encoding in ['utf-16le', 'utf-8', 'latin-1']:
try:
# 解码
raw_text = buffer.tobytes().decode(encoding, errors='ignore').rstrip('\x00')
# 使用正则表达式查找 "数字 + 月" 格式的部分
match = re.search(r'\d+月', raw_text)
if match:
return match.group()
# 使用正则表达式提取有效字符(中文、数字、字母和常见标点)
filtered_text = re.findall(r'[\w\u4e00-\u9fa5]+', raw_text)
# 返回匹配到的第一个有效部分
if filtered_text:
return filtered_text[0].strip()
except UnicodeDecodeError:
continue # 尝试下一个编码方式
# 如果所有解码方式都失败,返回 None
return None
# 设置 ComboBox 的值
def set_combobox_value(combo_handle, value):
items = get_combobox_items(combo_handle)
try:
index = items.index(value)
win32gui.SendMessage(combo_handle, win32con.CB_SETCURSEL, index, 0)
except ValueError:
raise Exception("Value not found in ComboBox")
def set_path_and_save(filename): def set_path_and_save(filename):
parent_hwnd = win32gui.FindWindow(None, '另存为') parent_hwnd = win32gui.FindWindow(None, '保存Excel文件')
if parent_hwnd == 0: if parent_hwnd == 0:
raise HTTPException(status_code=404, detail="没有找到保存报告的窗口,请检查!") raise HTTPException(status_code=404, detail="没有找到保存报告的窗口,请检查!")
# 这里假设“地址:”是你需要的部分标题 combo_handle = get_combobox_handle(parent_hwnd)
address_hwnds = find_child_window_by_partial_title(parent_hwnd, "地址:") #logging.info(f"ComboBox Items: {get_combobox_items(combo_handle)}")
logging.info(f"Current Selected: {get_combobox_selected(combo_handle)}")
# 确保找到的窗口句柄有效 local_file_path = "C:\\RationalDMIS64\\Output\\" + get_combobox_selected(combo_handle) + "\\" + filename
if not address_hwnds:
raise HTTPException(status_code=404, detail="未找到地址框,请联系管理员!")
# 假设找到的第一个窗口是目标组件
address_hwnd = address_hwnds[0]
logging.info(f"找到地址框地址: {win32gui.GetWindowText(address_hwnd)}")
# 设置路径
local_file_path = os.path.join(win32gui.GetWindowText(address_hwnd).split(' ')[1], filename)
logging.info(f"设置路径: {local_file_path}") logging.info(f"设置路径: {local_file_path}")
path_hwnds = find_child_window(parent_hwnd, 'Edit') path_hwnds = find_child_window(parent_hwnd, 'Edit')
@@ -115,7 +182,6 @@ def wait_for_file_to_save(filepath, timeout=30):
def upload_file_to_ftp(local_file): def upload_file_to_ftp(local_file):
if not os.path.isfile(local_file): if not os.path.isfile(local_file):
raise HTTPException(status_code=204, detail="文件未找到") raise HTTPException(status_code=204, detail="文件未找到")
@@ -157,4 +223,4 @@ async def upload_file(request: FileUploadRequest):
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8999)