Files
gzth/yuthon_issue/models/inherit_room_booking.py
T
2026-08-13 16:21:07 +08:00

562 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from datetime import timedelta
from datetime import datetime, timedelta, date
class RoomBooking(models.Model):
_inherit = 'room.booking'
is_issue = fields.Boolean(string="是否关联议题", default=True)
issue_meeting_line_ids = fields.One2many('issue.meeting.line', 'meeting_booking_id', string="关联议题明细行")
number_of_meetings = fields.Integer(string='会议次数')
employee_ids2 = fields.Many2many('hr.employee', 'room_booking_employee_rel2', 'custom_id', 'employee_id',
string='复制人员')
minutes_id = fields.Many2one('issue.meeting.minutes', string='会议纪要流程')
is_company = fields.Boolean(string="本公司", default=True, compute='_compute_is_company',
search='_search_part_of_company')
@api.onchange('number_of_meetings')
def _onchange_number_of_meetings(self):
if self.meeting_type_id and '班子' in str(self.meeting_type_id.name):
if self.issue_meeting_line_ids and self.start_datetime:
suffix_list = ['(通过)', '(未通过)']
for line in self.issue_meeting_line_ids:
if not line.issue_library_id:
continue
original_char = line.issue_library_id.class_char or ""
if not original_char:
continue
# 清理HTML标签
clean_char = original_char.replace('</span>', '').replace(
'<span style="background:#E3F2FD;color:#1565C0;padding:2px 8px;border-radius:4px;">', '')
# 提取已有后缀
existing_suffix = ""
for suffix in suffix_list:
if clean_char.endswith(suffix):
existing_suffix = suffix
break
# 重建 class_char(更新次数)
new_class_char = str(self.start_datetime.date()) + f'第{self.number_of_meetings}次' + self.meeting_type_id.name
line.issue_library_id.class_char = new_class_char + existing_suffix
@api.onchange('meeting_type_id')
def _onchange_meeting_type_id(self):
if self.meeting_type_id:
host_role_id = self.meeting_type_id.host_id.id
host_employee = self.env['hr.employee'].search([
'|',
('user_role_ids', 'in', host_role_id),
('user_role_id', '=', host_role_id),
('company_id', '=', self.meeting_type_id.company_id.id)
], limit=1)
room_role_ids = self.meeting_type_id.room_users_ids.ids
room_employees = self.env['hr.employee'].search([
'|',
('user_role_ids', 'in', room_role_ids),
('user_role_id', 'in', room_role_ids),
('id', '!=', host_employee.id if host_employee else False),
('company_id', '=', self.meeting_type_id.company_id.id)
])
room_user_ids = room_employees.mapped('user_id.id')
attendee_role_ids = self.meeting_type_id.attendees_ids.ids
attendee_employees = self.env['hr.employee'].search([
'|',
('user_role_ids', 'in', attendee_role_ids),
('user_role_id', 'in', attendee_role_ids),
('company_id', '=', self.meeting_type_id.company_id.id)
])
attendee_user_ids = attendee_employees.mapped('user_id.id')
self.host = host_employee.id
self.room_users_ids = [(6, 0, room_user_ids)]
if self.issue_meeting_line_ids:
for line in self.issue_meeting_line_ids:
line.attendees_ids = [(6, 0, attendee_employees.ids)]
else:
self.room_users_ids = [(5, 0, 0)]
self.host = False
if self.issue_meeting_line_ids:
for line in self.issue_meeting_line_ids:
line.attendees_ids = [(5, 0, 0)]
@api.constrains('name', 'issue_meeting_line_ids')
def _check_name(self):
for record in self:
if record.issue_meeting_line_ids:
for meet in record.issue_meeting_line_ids:
if meet.issue_library_id:
meeting_type_name = str(record.meeting_type_id.name).strip()
if '党' in meeting_type_name:
if meet.issue_library_id.is_party and not meet.issue_library_id.is_party_passed:
party_value = str(record.start_datetime.date()) + record.meeting_type_id.name
meet.issue_library_id.party_char = party_value
meet.issue_library_id.issue_state_party = 'meeting'
meet.issue_library_id.issue_state = False
if '班子' in meeting_type_name:
if meet.issue_library_id.is_class and not meet.issue_library_id.is_class_passed:
class_value = str(record.start_datetime.date()) + f'第{record.number_of_meetings}次' + record.meeting_type_id.name
meet.issue_library_id.class_char = class_value
meet.issue_library_id.issue_state_class = 'meeting'
meet.issue_library_id.issue_state = False
if '董事' in meeting_type_name:
if meet.issue_library_id.is_boss and not meet.issue_library_id.is_boss_passed:
boss_value = str(record.start_datetime.date()) + record.meeting_type_id.name
meet.issue_library_id.boss_char = boss_value
meet.issue_library_id.issue_state_boss = 'meeting'
meet.issue_library_id.issue_state = False
elif '套' in meeting_type_name:
if meet.issue_library_id.is_party and (meet.issue_library_id.is_party_passed == 'reject' or not meet.issue_library_id.is_party_passed):
party_value = str(record.start_datetime.date()) + record.meeting_type_id.name
meet.issue_library_id.party_char = party_value
meet.issue_library_id.issue_state_party = 'meeting'
meet.issue_library_id.issue_state = False
elif meet.issue_library_id.is_class and (meet.issue_library_id.is_class_passed == 'r eject' or not meet.issue_library_id.is_class_passed):
class_value = str(record.start_datetime.date()) + f'第{record.number_of_meetings}次' + record.meeting_type_id.name
meet.issue_library_id.class_char = class_value
meet.issue_library_id.issue_state_class = 'meeting'
meet.issue_library_id.issue_state = False
if self.code:
exists = self.env['issue.library.approval.line'].search([
('issue_library_id', '=', meet.issue_library_id.id),
('code', '=', self.code)
], limit=1)
if not exists:
self.env['issue.library.approval.line'].create({
'issue_library_id': meet.issue_library_id.id,
'code': self.code,
'approval_date': fields.Datetime.now() + timedelta(hours=8),
})
@api.constrains('meeting_type_id', 'start_datetime', 'stop_datetime', 'issue_meeting_line_ids')
def _check_meeting_time_order(self):
for record in self:
if not (record.meeting_type_id and record.start_datetime and record.stop_datetime):
continue
if not record.issue_meeting_line_ids:
continue
meeting_type_name = str(record.meeting_type_id.name).strip()
is_class_meeting = '班子' in meeting_type_name
is_boss_meeting = '董事' in meeting_type_name
if not (is_class_meeting or is_boss_meeting):
continue
for line in record.issue_meeting_line_ids:
if not line.issue_library_id:
continue
issue = line.issue_library_id
# 通过 issue.meeting.line 找到该议题关联的其他 room.booking
other_lines = self.env['issue.meeting.line'].search([
('issue_library_id', '=', issue.id),
('meeting_booking_id', '!=', record.id),
])
other_bookings = other_lines.mapped('meeting_booking_id')
def _has_overlap(b1_start, b1_stop, b2_start, b2_stop):
"""两个时间范围是否有重叠"""
return b1_start < b2_stop and b2_start < b1_stop
def _check_time_order_and_overlap(target_bookings, target_type_name):
"""
检查前置会议是否早于当前会议,且时间不重叠
target_bookings: 前置会议记录集
target_type_name: 前置会议类型名称(用于错误提示)
"""
for tb in target_bookings:
if not (tb.start_datetime and tb.stop_datetime):
continue
# 前置会议必须早于当前会议
if tb.start_datetime >= record.start_datetime:
raise UserError(
f'议题“{issue.name}”的{target_type_name}会议时间({tb.start_datetime + timedelta(hours=8)} ~ {tb.stop_datetime + timedelta(hours=8)})'
f'必须早于{record.meeting_type_id.name}会议时间({record.start_datetime + timedelta(hours=8)} ~ {record.stop_datetime + timedelta(hours=8)})!'
)
# 时间不能重叠
if _has_overlap(record.start_datetime, record.stop_datetime,
tb.start_datetime, tb.stop_datetime):
raise UserError(
f'议题“{issue.name}”的{target_type_name}会议时间({tb.start_datetime + timedelta(hours=8)} ~ {tb.stop_datetime + timedelta(hours=8)})'
f'与{record.meeting_type_id.name}会议时间({record.start_datetime + timedelta(hours=8)} ~ {record.stop_datetime + timedelta(hours=8)})有重叠!'
)
# ========== 班子会:党的 party_char 非空(党已上会),班子必须晚于党,且时间不能重叠 ==========
if is_class_meeting and line.issue_library_id.is_party == True:
clean_party = re.sub(r'<[^>]+>', '', issue.party_char or '').strip()
if clean_party:
party_bookings = other_bookings.filtered(
lambda b: b.meeting_type_id and '党' in str(b.meeting_type_id.name)
and b.start_datetime and b.stop_datetime
)
_check_time_order_and_overlap(party_bookings, '党')
# ========== 董事会:如果班子已上会,董事必须晚于班子;否则如果党已上会,董事必须晚于党 ==========
if is_boss_meeting and line.issue_library_id.is_class == True:
clean_class = re.sub(r'<[^>]+>', '', issue.class_char or '').strip()
if clean_class:
class_bookings = other_bookings.filtered(
lambda b: b.meeting_type_id and '班子' in str(b.meeting_type_id.name)
and b.start_datetime and b.stop_datetime
)
_check_time_order_and_overlap(class_bookings, '班子')
else:
clean_party = re.sub(r'<[^>]+>', '', issue.party_char or '').strip()
if clean_party and line.issue_library_id.is_party == True:
party_bookings = other_bookings.filtered(
lambda b: b.meeting_type_id and '党' in str(b.meeting_type_id.name)
and b.start_datetime and b.stop_datetime
)
_check_time_order_and_overlap(party_bookings, '党')
@api.depends_context('uid', 'company')
@api.depends('company_id')
def _compute_is_company(self):
active_company_ids = self.env.companies
for employee in self:
employee.is_company = employee.company_id in active_company_ids
def _search_part_of_company(self, operator, value):
company_ids = self.env.companies.ids
if not value:
operator = '!=' if operator == '=' else '='
if operator == '=':
return [('company_id', 'in', company_ids)]
else:
return [('company_id', 'not in', company_ids)]
def notice_room(self):
booking_records = self.search([
('work_end', '=', True),
('task_time_ids', '!=', False),
('start_datetime', '!=', False)
])
if not booking_records:
return
now = fields.Datetime.now() + timedelta(hours=8) # 转成北京时间,与 meeting_start 保持同一时区
for booking in booking_records:
remind_time_thresholds = []
for task_time in booking.task_time_ids:
if not task_time.number or task_time.number <= 0:
continue
if task_time.task_unit == 'min':
threshold = timedelta(minutes=task_time.number)
elif task_time.task_unit == 'hour':
threshold = timedelta(hours=task_time.number)
elif task_time.task_unit == 'day':
threshold = timedelta(days=task_time.number)
else:
continue
remind_time_thresholds.append(threshold)
if not remind_time_thresholds:
continue
meeting_start = fields.Datetime.to_datetime(booking.start_datetime) + timedelta(hours=8)
actual_diff = meeting_start - now
if actual_diff.total_seconds() < 0:
continue
need_remind = False
for threshold in remind_time_thresholds:
diff_seconds = abs(actual_diff.total_seconds() - threshold.total_seconds())
if diff_seconds <= 60: # 误差≤60秒即判定为匹配
need_remind = True
break
if need_remind:
remind_users = self._get_remind_users(booking)
employee_ids = self.env['hr.employee'].search([('user_id', 'in', remind_users.ids)])
url = f'https://oa.thtzjt.com/web#id={booking.id}&menu_id=9539&action=811&model=room.booking&view_type=form' # 用当前会议记录,避免取到第一条
data = booking.name + '\n'
time_str = fields.Datetime.to_datetime(booking.start_datetime + timedelta(hours=8)).strftime('%H:%M:%S')
data += f'会议时间:{time_str}'
for employee_id in employee_ids:
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='task',
user_id=employee_id.wecom_userid,
title="会议通知", description=data,
url=url, btntxt="详细信息")
def _get_remind_users(self, booking):
remind_user_ids = set()
# ① 组织者
if booking.organizer_id:
remind_user_ids.add(booking.organizer_id.id)
# ② 记录人
if booking.user_id:
remind_user_ids.add(booking.user_id.id)
# ③ 出席人员
if booking.room_users_ids:
remind_user_ids.update(booking.room_users_ids.ids)
# ④ 传达人
for line in booking.issue_meeting_line_ids:
if line.employee_id and line.employee_id.user_id:
remind_user_ids.add(line.employee_id.user_id.id)
# ⑤ 列席人员
for line in booking.issue_meeting_line_ids:
if line.attendees_ids:
for employee in line.attendees_ids:
remind_user_ids.add(employee.user_id.id)
# 列席群组
for line in booking.issue_meeting_line_ids:
if line.users_role_group_ids:
role_group_roles = line.users_role_group_ids.mapped('user_role_ids')
role_employees = self.env['hr.employee'].search([
'|',
('user_role_id', 'in', role_group_roles.ids),
('user_role_ids', 'in', role_group_roles.ids)
])
remind_user_ids.add(role_employees.mapped('user_id.id'))
# ⑥ 主持人
if booking.host and booking.host.user_id:
remind_user_ids.add(booking.host.user_id.id)
return self.env['res.users'].browse(list(remind_user_ids)).filtered(lambda u: u.active)
@api.depends('number_of_meetings', 'meeting_type_id', 'start_datetime')
def _compute_name(self):
if self.meeting_type_id and self.start_datetime:
if self.number_of_meetings:
self.name = str(self.start_datetime.date()) + f'第{self.number_of_meetings}次' + self.meeting_type_id.name
else:
self.name = str(self.start_datetime.date()) + self.meeting_type_id.name
else:
self.name = ''
def end_approval(self):
self.write({'work_end': True})
def bottom_meeting_approval(self):
library_data = []
for library in self.issue_meeting_line_ids:
library_data.append((0, 0, {
'minutes_id': library.issue_library_id.id,
'decision_results': library.decision_results,
'overview': library.decision_details,
}))
return {
'name': '会议纪要审批',
'type': 'ir.actions.act_window',
'view_mode': "form",
'res_model': 'issue.meeting.minutes',
'context': {
'default_meeting_id': self.id,
'default_room_users_ids': [(6, 0, self.room_users_ids.ids)],
'default_host': self.host.id,
'default_meeting_type_id': self.meeting_type_id.id,
'default_meeting_date': self.start_datetime,
'default_room_id': self.room_id.id,
'default_number_of_meetings': self.number_of_meetings,
'default_meeting_minutes_ids': library_data
}}
@api.constrains('issue_meeting_line_ids', 'room_id', 'meeting_type_id', 'start_datetime')
def _constrains_create_room_line(self):
for main_record in self:
if not (main_record.issue_meeting_line_ids and main_record.room_id and main_record.meeting_type_id):
continue
for line in main_record.issue_meeting_line_ids:
if not line.issue_library_id:
continue
exists_line = self.env['issue.library.room.line'].search([
('issue_library_id', '=', line.issue_library_id.id),
('room_booking_id', '=', main_record.id),
], limit=1)
if not exists_line:
self.env['issue.library.room.line'].create({
'issue_library_id': line.issue_library_id.id,
'room_booking_id': main_record.id,
'room_id': main_record.room_id.id,
'meeting_type_id': main_record.meeting_type_id.id,
'approval_date': main_record.start_datetime,
})
class IssueMeetingLine(models.Model):
_name = 'issue.meeting.line'
_description = '关联议题明细行'
_order = 'sequence asc'
_rec_name = 'issue_library_name'
meeting_booking_id = fields.Many2one('room.booking', string='关联会议室')
sequence = fields.Char(string='序号', store=True)
issue_library_id = fields.Many2one('issue.library', string='议题')
issue_library_name = fields.Char(related='issue_library_id.name', string='议题')
issue_library_text = fields.Text(string='议题内容', compute='_compute_issue_library_text', store=False)
@api.depends('issue_library_id')
def _compute_issue_library_text(self):
for rec in self:
rec.issue_library_text = rec.issue_library_id.name or ''
employee_id = fields.Many2one('hr.employee', string='传达人')
designated_person_ids = fields.Many2many(
'hr.employee',
'issue_meeting_line_designated_person_rel'
'line_id',
'employee_id',
string='指定人员'
)
attendees_ids = fields.Many2many(
'hr.employee',
'issue_meeting_line_attendees_rel',
'line_id',
'employee_id',
string='列席人员'
)
users_role_group_ids = fields.Many2many(comodel_name="soong.user.role.group", string="列席群组")
is_sanda = fields.Boolean(string="三重一大")
decision_results = fields.Selection([('agree', '通过'), ('rejected', '驳回')], string="决策结果")
decision_details = fields.Text(string="决策详情")
def copy_line(self):
for booking in self:
if not booking.meeting_booking_id.employee_ids2:
raise UserError("请添加需要复制的人员")
if booking.meeting_booking_id.employee_ids2:
booking.attendees_ids = [(6, 0, booking.meeting_booking_id.employee_ids2.ids)]
@api.onchange('issue_library_id')
def onchange_issue_library_id(self):
if self.issue_library_id:
dept_manager = self.env['hr.employee'].search([
('active', '=', True),
('user_role_id.name', '=', '部门经理'),
'|',
('department_id', '=', self.issue_library_id.department_id.id),
('department_ids', 'in', [self.issue_library_id.department_id.id])
], limit=1)
if dept_manager:
self.employee_id = dept_manager.id
def open_record(self):
form_id = self.env.ref('yuthon_issue.view_issue_library_form').id
return {
'type': 'ir.actions.act_window',
'name': '议题详情',
'res_model': 'issue.library',
'view_mode': 'form',
'views': [(form_id, 'form')],
'res_id': self.issue_library_id.id,
}
@api.constrains('decision_results', 'meeting_booking_id', 'issue_library_id')
def _check_decision_results(self):
for record in self:
if not record.meeting_booking_id.meeting_type_id:
continue
meeting_type_name = str(record.meeting_booking_id.meeting_type_id.name).strip()
suffix_list = ['(通过)', '(未通过)']
record.meeting_booking_id._check_name()
is_push = True
# 统一处理通过/不通过标识
if record.decision_results == 'agree':
is_agree = 'pass'
agree_name = '(通过)'
elif record.decision_results == 'rejected':
is_agree = 'reject'
agree_name = '(未通过)'
else:
is_agree = None
agree_name = ''
record.issue_library_id.is_push_depart = True
record.issue_library_id.issue_state = False
# 2. 党委会相关
if '党' in meeting_type_name:
if not record.issue_library_id.is_push_party:
record.issue_library_id.is_push_party = True
record.issue_library_id.push_invisible = True
party_value = str(record.meeting_booking_id.start_datetime.date()) + record.meeting_booking_id.meeting_type_id.name
record.issue_library_id.party_char = party_value
if record.issue_library_id.is_class:
record.issue_library_id.is_push_class = is_push
elif record.issue_library_id.is_boss:
record.issue_library_id.is_push_boss = is_push
record.issue_library_id.is_party_passed = is_agree
original_party_char = record.issue_library_id.party_char or ""
clean_party_char = original_party_char
for suffix in suffix_list:
clean_party_char = clean_party_char.removesuffix(suffix)
clean_party_char = clean_party_char.replace('</span>', '').replace('<span style="background:#FFEBE8;color:#D32F2F;padding:2px 8px;border-radius:4px;">', '')
record.issue_library_id.party_char = clean_party_char + agree_name
record.issue_library_id.issue_state_party = 'on_meet'
# 3. 班子会相关
if '班子' in meeting_type_name:
if not record.issue_library_id.is_push_class:
record.issue_library_id.is_push_class = True
record.issue_library_id.push_invisible = True
party_value = str(record.meeting_booking_id.start_datetime.date()) + record.meeting_booking_id.meeting_type_id.name
record.issue_library_id.class_char = party_value
if record.issue_library_id.is_boss:
record.issue_library_id.is_push_boss = is_push
record.issue_library_id.is_class_passed = is_agree
original_class_char = record.issue_library_id.class_char or ""
clean_class_char = original_class_char
for suffix in suffix_list:
clean_class_char = clean_class_char.removesuffix(suffix)
clean_class_char = clean_class_char.replace('</span>', '').replace('<span style="background:#E3F2FD;color:#1565C0;padding:2px 8px;border-radius:4px;">', '')
record.issue_library_id.class_char = clean_class_char + agree_name
record.issue_library_id.issue_state_class = 'on_meet'
# 4. 董事会相关
if '董事' in meeting_type_name:
if not record.issue_library_id.is_push_boss:
record.issue_library_id.is_push_boss = True
record.issue_library_id.push_invisible = True
party_value = str(record.meeting_booking_id.start_datetime.date()) + record.meeting_booking_id.meeting_type_id.name
record.issue_library_id.boss_char = party_value
record.issue_library_id.is_boss_passed = is_agree
original_boss_char = record.issue_library_id.boss_char or ""
clean_boss_char = original_boss_char
for suffix in suffix_list:
clean_boss_char = clean_boss_char.removesuffix(suffix)
clean_boss_char = clean_boss_char.replace('</span>', '').replace('<span style="background:#F3F4F6;color:#4B5563;padding:2px 8px;border-radius:4px;">', '')
record.issue_library_id.boss_char = clean_boss_char + agree_name
record.issue_library_id.issue_state_boss = 'on_meet'
# 套开相关
if '套' in meeting_type_name:
if (record.issue_library_id.is_party and
(
record.issue_library_id.is_party_passed == 'reject' or not record.issue_library_id.is_party_passed)):
if record.issue_library_id.is_class:
record.issue_library_id.is_push_class = is_push
record.issue_library_id.is_party_passed = is_agree
original_party_char = record.issue_library_id.party_char or ""
clean_party_char = original_party_char
for suffix in suffix_list:
clean_party_char = clean_party_char.removesuffix(suffix)
clean_party_char = clean_party_char.replace('</span>', '').replace('<span style="background:#FFEBE8;color:#D32F2F;padding:2px 8px;border-radius:4px;">', '')
record.issue_library_id.party_char = clean_party_char + agree_name
record.issue_library_id.issue_state_party = 'on_meet' if is_push else 'will'
elif (record.issue_library_id.is_class and
(
record.issue_library_id.is_class_passed == 'reject' or not record.issue_library_id.is_class_passed)):
# 班子会逻辑
record.issue_library_id.is_class_passed = is_agree
original_class_char = record.issue_library_id.class_char or ""
clean_class_char = original_class_char
for suffix in suffix_list:
clean_class_char = clean_class_char.removesuffix(suffix)
clean_class_char = clean_class_char.replace('</span>', '').replace('<span style="background:#E3F2FD;color:#1565C0;padding:2px 8px;border-radius:4px;">', '')
record.issue_library_id.class_char = clean_class_char + agree_name
record.issue_library_id.issue_state_class = 'on_meet' if is_push else 'will'