Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
pengyuthon
2026-03-05 23:14:15 +08:00
17 changed files with 482 additions and 136 deletions
@@ -582,22 +582,6 @@ class SoongWorkflowMixin(models.AbstractModel):
if hasattr(btn_rec.node_id, target_field) and btn_rec.node_id[target_field]:
comment_note_id = btn_rec.node_id[target_field].id
# 加签人员过滤
if self.log_ids:
log_node_ids = self.log_ids.mapped('node_id.id')
log_nodes = self.env['soong.workflow.node'].browse(log_node_ids)
all_node_employees = log_nodes.mapped('employee_ids')
else:
current_node = node_list[state_list.index(state)]
all_node_employees = current_node.employee_ids
log_done_by_users = self.log_ids.mapped('done_by')
log2_done_by_users = self.log_ids2.mapped('done_by')
all_done_by_users = log_done_by_users | log2_done_by_users
done_by_employees = self.env['hr.employee'].search([
('user_id', 'in', all_done_by_users.ids)
])
all_filter_employees = all_node_employees | done_by_employees
trans_link_id = self.env['trans.link'].search(trans_domain, limit=1).sorted('sequence').id if btn_rec.name == '转交' else False
return_link_id = self.env['trans.link'].search([('link_type', '=', 'return'), ('record_id', '=', self.id)], limit=1).sorted('sequence').id if btn_rec.name == '退回' else False,
return {
@@ -615,7 +599,6 @@ class SoongWorkflowMixin(models.AbstractModel):
"default_workflow_record_id": workflow_record_id.id,
'default_illustrate':btn_rec.node_id.illustrate,
"default_res_model": self._name,
"default_filter_personnel_ids": [(6, 0, all_filter_employees.ids)],
"default_node_btn_id": btn_rec.id,
"default_is_supervising": is_supervising,
"default_trans_link_id": trans_link_id,
+38 -40
View File
@@ -83,26 +83,9 @@ class SoongWorkflowNode(models.Model):
res_bg_color = fields.Char(compute="_compute_custom_colors", readonly=True, string="Backgroung Color")
res_label_color = fields.Char(compute="_compute_custom_colors", readonly=True, string="Label Color")
is_carbon_copy = fields.Boolean(string="抄送/传阅")
is_archive = fields.Boolean(string="归档")
illustrate = fields.Text(string="说明")
is_apostille = fields.Boolean(string="加签", default=False)
department1_ids = fields.Many2many(
"hr.department",
string="抄送部门",
relation="soong_workflow_node_department_rel",
column1="node_id",
column2="department_id"
)
user_role_ids = fields.Many2many("soong.user.role", string="抄送角色", )
users_role_group_ids = fields.Many2many('soong.user.role.group', string='抄送角色组')
employee_ids = fields.Many2many(
'hr.employee',
string='抄送/传阅人员',
relation="soong_workflow_node_employee_total_rel",
column1="node_id",
column2="employee_total_id"
)
is_end = fields.Boolean(string="结束", default=False)
is_confirm = fields.Boolean(string="确认", default=False)
department_id = fields.Many2many(
@@ -116,32 +99,10 @@ class SoongWorkflowNode(models.Model):
return_comment_id = fields.Many2one('soong.note', string="退回固定意见")
confirm_comment_id = fields.Many2one('soong.note', string="确认固定意见")
end_comment_id = fields.Many2one('soong.note', string="结束固定意见")
approval_concatenation = fields.Selection([('chuan', '串申'), ('bing', '并申')], string="审批形式", default="bing",
help='并:任何人均给意见,最后一个完成的人转交到下一节点.'
'串:都会收到消息,但任意一个人转交后,即到下一节点,其他人将不再显示,相当于收回.')
@api.onchange('department1_ids', 'user_role_ids', 'users_role_group_ids', 'employee1_ids')
def _onchange_sync_employee_ids(self):
for record in self:
final_employees = self.env['hr.employee']
role_group_roles = record.users_role_group_ids.mapped('user_role_ids')
all_target_roles = record.user_role_ids | role_group_roles
if all_target_roles:
role_employees = self.env['hr.employee'].search([
'|',
('user_role_id', 'in', all_target_roles.ids),
('user_role_ids', 'in', all_target_roles.ids)
])
final_employees |= role_employees
if record.department1_ids:
dept_employees = self.env['hr.employee'].search([
('department_id', 'in', record.department1_ids.ids)
])
final_employees |= dept_employees
record.employee_ids = final_employees if final_employees else [(5, 0, 0)]
@api.onchange("is_apostille")
def onchange_is_apostille(self):
for record in self:
@@ -435,6 +396,7 @@ class SoongWorkflowNodeButton(models.Model):
],
)
is_review_invisible = fields.Boolean(string="使用范围", default=False)
is_node_automatic = fields.Boolean('是否自动选择所有人员', help='默认否,选择是之后,前节点转交是会自动带出本节点的所有匹配的人员。', default=False)
illustrate = fields.Text(string="说明")
reviewer_id = fields.Many2one(comodel_name="res.users", string="Reviewer")
reviewer_group_id = fields.Many2one(comodel_name="res.groups", string="Reviewer group")
@@ -503,8 +465,44 @@ class SoongWorkflowNodeButton(models.Model):
is_dep = fields.Boolean('按指定部门')
role_id = fields.Many2one(comodel_name="soong.user.role.group", string="角色")
specify_field_id = fields.Many2one(comodel_name="ir.model.fields", string="指定字段")
is_carbon_copy = fields.Boolean(string="抄送/传阅")
department1_ids = fields.Many2many(
"hr.department",
string="抄送部门",
relation="soong_workflow_node_department_rel",
column1="node_id",
column2="department_id"
)
user_role_ids = fields.Many2many("soong.user.role", string="抄送角色", )
cc_users_role_group_ids = fields.Many2many(
'soong.user.role.group',
'soong_workflow_node_button_cc_groups_rel',
'button_id',
'group_id',
string='抄送角色组'
)
employee_ids = fields.Many2many('hr.employee', string='抄送/传阅人员')
@api.onchange('department1_ids', 'user_role_ids', 'users_role_group_ids', 'employee1_ids')
def _onchange_sync_employee_ids(self):
for record in self:
final_employees = self.env['hr.employee']
role_group_roles = record.users_role_group_ids.mapped('user_role_ids')
all_target_roles = record.user_role_ids | role_group_roles
if all_target_roles:
role_employees = self.env['hr.employee'].search([
'|',
('user_role_id', 'in', all_target_roles.ids),
('user_role_ids', 'in', all_target_roles.ids)
])
final_employees |= role_employees
if record.department1_ids:
dept_employees = self.env['hr.employee'].search([
('department_id', 'in', record.department1_ids.ids)
])
final_employees |= dept_employees
record.employee_ids = final_employees if final_employees else [(5, 0, 0)]
def update_user_id(self):
button_ids = self.env["soong.workflow.node.button"].browse(
+141 -41
View File
@@ -166,7 +166,7 @@ class SoongWorkflowRecord(models.Model):
current_uid = self.env.user.id
for record in self:
record.is_done = False
condition1 = record.soong_state = 'done'
condition1 = True if record.soong_state =='done' else False
condition2 = not record.reclaim_state
condition3 = False
if record.users_ids:
@@ -478,6 +478,32 @@ class SoongWorkflowRecord(models.Model):
def _compute_is_manager(self):
self.is_manager = self.env.user.has_group('soong_workflow.group_soong_workflow_manager')
def update_users_ids(self):
record_ids = self.env["soong.workflow.record"].browse(
self._context.get('active_ids', self._context.get('active_id', []))
)
if not record_ids:
return
lin_weidong = self.env['res.users'].search([('name', '=', '林伟东')], limit=1)
employee_id = self.env['hr.employee'].search([('name', '=', '林伟东')], limit=1)
if not lin_weidong:
return
for record in record_ids:
self.env['soong.workflow.log2'].create({
'model': record.res_model,
'res_id': record.res_id,
'node_id': record.node_id.id,
'done_by': lin_weidong.id,
'approval_state': '0',
})
if lin_weidong not in record.users_ids:
record.users_ids |= lin_weidong
if employee_id not in record.employee_ids:
record.employee_ids |= employee_id
def open_record(self):
return {
"type": "ir.actions.act_window",
@@ -494,29 +520,39 @@ class SoongWorkflowRecord(models.Model):
('soong_state', '!=', 'active'),
])
for record in all_records:
# 校验关联记录是否存在
if not record.res_model or not record.res_id:
continue
record_id = self.env[record.res_model].browse(record.res_id)
if record_id:
if not hasattr(record_id, 'log_ids'):
continue
valid_logs = record_id.log_ids.filtered(lambda l: l.node_btn_name != '1')
for log in valid_logs:
if log.done_by.name == self.env.user.name:
if log.name == '归档' and (log.approval_state == '0' or log.approval_state == '1'):
record.write({'soong_state': 'to_active'})
elif log.name == '归档' and log.approval_state == '2':
record.write({'soong_state': 'active'})
elif log.approval_state == '0' or log.approval_state == '1':
record.write({'soong_state': 'to_do'})
else:
record.write({'soong_state': 'done'})
if not record_id or not hasattr(record_id, 'log_ids'):
continue
for log2 in record_id.log_ids2:
if log2.done_by.name == self.env.user.name:
if log2.approval_state == '2':
record.write({'soong_state': 'done'})
elif log2.approval_state in ['0', '1']:
record.write({'soong_state': 'to_do'})
current_user_logs = record_id.log_ids.filtered(
lambda l: l.done_by.id == self.env.user.id
)
if not current_user_logs:
continue
approval_states = current_user_logs.mapped('approval_state')
if any(state in ['0', '1'] for state in approval_states):
record.write({'soong_state': 'to_do'})
elif all(state == '2' for state in approval_states):
record.write({'soong_state': 'done'})
for log in current_user_logs:
if log.name == '归档':
if log.approval_state == '0' or log.approval_state == '1':
record.write({'soong_state': 'to_active'})
elif log.approval_state == '2':
record.write({'soong_state': 'active'})
break
for log2 in record_id.log_ids2:
if log2.done_by.name == self.env.user.name:
if log2.approval_state == '2':
record.write({'soong_state': 'done'})
elif log2.approval_state in ['0', '1']:
record.write({'soong_state': 'to_do'})
record._compute_is_done()
record._compute_is_to_do()
return super(SoongWorkflowRecord, self).get_view(view_id, view_type, **options)
def to_do_process_reminders(self):
@@ -626,35 +662,100 @@ class SoongWorkflowRecord(models.Model):
m = local_time.minute
if not (h == 17 and 25 <= m <= 35):
return
menu_id = self.env['ir.ui.menu'].search([('name', '=', '待办流程')])
target_url = f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}'
rec_ids = self.env["soong.workflow.record"].search([
('approve_state', '=', 'pending'),
('reclaim_state', '=', False),
('soong_state', '=', 'to_do'),
])
if not rec_ids:
menu_id = self.env['ir.ui.menu'].search([('name', '=', '待办流程')], limit=1)
if not menu_id:
return
employee_task_count = {}
target_url = f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}'
active_employees = self.env['hr.employee'].search([('user_state', '=', 'in')])
active_employee_ids = set(active_employees.ids)
if not active_employees:
return
# 映射1:员工ID → 关联的用户ID(hr.employee的user_id字段)
emp_to_user = {emp.id: emp.user_id.id for emp in active_employees if emp.user_id}
active_employee_ids = set(emp_to_user.keys())
record_ids = self.env["soong.workflow.record"].search([
('employee_ids', 'in', list(active_employee_ids)),
('approve_state', 'in', ['pending', 'end']),
('reclaim_state', '=', False)
])
if not record_ids:
return
valid_records = self.env["soong.workflow.record"]
for record in record_ids:
target_record = self.env[record.res_model].search([('id', '=', record.res_id)])
record_emp_ids = [emp.id for emp in record.employee_ids if emp.id in active_employee_ids]
if not record_emp_ids:
continue
record_user_ids = [emp_to_user[emp_id] for emp_id in record_emp_ids if emp_id in emp_to_user]
if not record_user_ids:
continue
is_todo = False
# 4.1 检查log_ids:仅筛选done_by属于当前记录关联用户的记录,任意0/1则算待办
if target_record.log_ids:
target_logs = target_record.log_ids.filtered(lambda l: l.done_by.id in record_user_ids)
for log in target_logs:
if log.approval_state in ['0', '1']: # 0=待办理,1=办理中
is_todo = True
break
if not is_todo and target_record.log_ids2:
# 过滤log_ids2:done_by在当前记录关联用户中(仅一条)
target_log2 = target_record.log_ids2.filtered(lambda l: l.done_by.id in record_user_ids)
if target_log2 and target_log2[0].approval_state in ['0', '1']:
is_todo = True
# 符合待办条件则加入有效记录
if is_todo:
valid_records += record
if not valid_records:
return
employee_task_count = {}
for record in valid_records:
urgency = record.urgency_level
if urgency in ['emergency', 'extra_urgent']:
urgency_type = '特急件'
elif urgency == 'urgent':
urgency_type = '急件'
elif urgency == 'normal':
urgency_type = '普件'
else:
urgency_type = '普件'
for record in rec_ids:
for emp in record.employee_ids:
if emp.id in active_employee_ids:
if emp.id not in employee_task_count:
employee_task_count[emp.id] = 0
employee_task_count[emp.id] += 1
emp_id = emp.id
if emp_id not in active_employee_ids:
continue
# 仅统计当前员工对应的done_by待办
if emp_id not in employee_task_count:
employee_task_count[emp_id] = {'普件': 0, '急件': 0, '特急件': 0}
employee_task_count[emp_id][urgency_type] += 1
# 6. 发送企业微信待办提醒
emp_ids = list(employee_task_count.keys())
employee_data = self.env['hr.employee'].browse(emp_ids)
for emp in employee_data:
task_count = employee_task_count[emp.id]
if not emp.wecom_userid:
task_stats = employee_task_count[emp.id]
total_count = sum(task_stats.values())
if total_count == 0 or not emp.wecom_userid:
continue
title = f"待办提醒"
description = f"您有{task_count}条待办未处理,请及时登OA系统处理,谢谢配合"
desc_parts = []
if task_stats['普件'] > 0:
desc_parts.append(f"{task_stats['普件']}条普件")
if task_stats['急件'] > 0:
desc_parts.append(f"{task_stats['急件']}条急件")
if task_stats['特急件'] > 0:
desc_parts.append(f"{task_stats['特急件']}条特急件")
description = f"您有{('、'.join(desc_parts))}待办未处理,请及时登OA系统处理,谢谢配合"
title = f"待办提醒(共{total_count}条)"
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='oa',
user_id=emp.wecom_userid,
@@ -663,7 +764,6 @@ class SoongWorkflowRecord(models.Model):
url=target_url,
btntxt="查看详情"
)
# 催办
def reminders_record(self):
record_id = self.env[self.res_model].search([('id', '=', self.res_id)])
@@ -40,15 +40,6 @@
<group>
<field name="flow_start"/>
<field name="flow_end" invisible="1"/>
<field name="is_carbon_copy"/>
<field name="user_role_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
<field name="users_role_group_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
<field name="department1_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
<field name="employee_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
<field name="is_apostille"/>
<field name="is_end"/>
<field name="is_confirm"/>
@@ -212,6 +203,18 @@
<field name="specify_field_id" domain="[('ttype', '=', 'many2one'), ('model', '=', invisible_expression_model)]"/>
</group>
</group>
<group>
<field name="is_node_automatic"/>
<field name="is_carbon_copy"/>
<field name="user_role_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
<field name="users_role_group_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
<field name="department1_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
<field name="employee_ids" widget="many2many_tags" invisible="not is_carbon_copy"
options="{'no_create': True}"/>
</group>
<notebook>
<page string="Condition">
<group>
@@ -549,11 +549,11 @@
<field name="state">code</field>
<field name="code">action = model.append_date()</field>
</record>
<!-- <record id="all_holiday_update_self_users_ids" model="ir.actions.server">-->
<!-- <field name="name">一次性</field>-->
<!-- <field name="model_id" ref="soong_workflow.model_soong_workflow_record"/>-->
<!-- <field name="binding_model_id" ref="soong_workflow.model_soong_workflow_record"/>-->
<!-- <field name="state">code</field>-->
<!-- <field name="code">action = model.update_self_users_ids()</field>-->
<!-- </record>-->
<record id="all_holiday_update_self_users_ids" model="ir.actions.server">
<field name="name">一次性</field>
<field name="model_id" ref="soong_workflow.model_soong_workflow_record"/>
<field name="binding_model_id" ref="soong_workflow.model_soong_workflow_record"/>
<field name="state">code</field>
<field name="code">action = model.update_users_ids()</field>
</record>
</odoo>
@@ -45,7 +45,6 @@ class SoongWorkflowCommentWizard(models.TransientModel):
)
employee_trans_domain_ids = fields.Many2many('hr.employee', 'employee_trans_domain_ids_rel', string="传阅人员", store=True)
filter_personnel_ids = fields.Many2many('hr.employee', 'filter_personnel_rel', 'custom_id', 'employee_id', string="过滤人员", store=True)
employee_ids = fields.Many2many('hr.employee', 'employee_ids_rel', 'custom_id', 'employee_id', string="传阅人员", store=True)
circulated_employee_ids = fields.Many2many('hr.employee', 'circulated_employee_rel', 'custom_id', 'employee_id', help='只可以看不可以评论,看过以后状态自动变为已阅', string="加签人员", store=True)
dep_manager_employee_ids = fields.Many2many('hr.employee', 'dep_manager_employee_rel', 'custom_id', 'employee_id', string="承办", store=True)
@@ -78,7 +77,14 @@ class SoongWorkflowCommentWizard(models.TransientModel):
if len(filtered_employees) == 1:
self.employee_ids = filtered_employees
else:
self.employee_ids = False
domain = [('id', 'in', self.employee_ids1.ids)]
if self.is_selection:
if self.is_this_company:
domain.append(('company_id', '=', self.company_id.id))
all_employees = self.env['hr.employee'].search(domain)
self.employee_ids = all_employees
else:
self.employee_ids = False
@api.constrains('employee_ids')
def _check_employee_ids(self):
@@ -189,6 +195,10 @@ class SoongWorkflowCommentWizard(models.TransientModel):
@api.onchange('trans_link_id')
def onchange_trans_employee_ids(self):
for record in self:
record.is_selection = False
record.is_this_company = True
if record.node_btn_id.employee_ids:
record.circulated_employee_ids = record.node_btn_id.employee_ids
if not record.trans_link_id:
continue
record_id = self.env[record.res_model].browse(record.res_id) if record.res_model and record.res_id else None
@@ -202,6 +212,35 @@ class SoongWorkflowCommentWizard(models.TransientModel):
record.approval_concatenation = node.approval_concatenation
record.employee_trans_domain_ids = employee_user_role_ids
record.employee_ids1 = employee_user_role_ids
workflow_id = link.node_button_id.workflow_id
node = workflow_id.node_ids.search([
('name', '=', link.name),
('workflow_id', '=', workflow_id.id)
])
button_priority_list = ["分发", "下发", "转交", "结束"]
button = False
if node.button_ids:
for btn_name in button_priority_list:
for btn in node.button_ids:
if btn.name == btn_name: # 匹配优先级按钮名称
is_in_scope = True
if not btn.is_review_invisible:
button = btn
break
if btn.is_review_invisible:
domain = eval(btn.invisible_expression)
model = record_id.env[record_id._name]
matched = model.search(domain + [('id', '=', record_id.id)])
is_in_scope = bool(matched)
if is_in_scope:
target_btn = btn
button = target_btn
break
if button:
if button.is_automatic_group:
record.is_this_company = False
if button.is_node_automatic:
record.is_selection = True
def _get_approvers_for_link(self, link, record_id=None):
"""
@@ -11,7 +11,6 @@
<group invisible="1">
<field name="approval_concatenation" widget="radio"/>
<field name="invalid_link_ids" widget="many2many_tags"/>
<field name="filter_personnel_ids" widget="many2many_tags"/>
<field name="user_role_ids"/>
<field name="comment"/>
</group>
@@ -46,7 +45,7 @@
<group>
<field name="is_this_company"/>
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}"
domain="[('company_id', '=', company_id), ('id', 'not in' , filter_personnel_ids)] if is_this_company else [('id', 'not in' , filter_personnel_ids)]"/>
domain="[('company_id', '=', company_id)] if is_this_company else []"/>
</group>
</group>
<group invisible="node_btn_name != '结束'">
@@ -63,7 +62,7 @@
</div>
<group>
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}"
domain="[('company_id', '=', company_id),('id', 'not in' , filter_personnel_ids)]"/>
domain="[('company_id', '=', company_id)]"/>
</group>
</group>
<group invisible="node_btn_name != '确认'">
@@ -77,7 +76,7 @@
</div>
<group>
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}"
domain="[('company_id', '=', company_id), ('id', 'not in' , filter_personnel_ids)]"/>
domain="[('company_id', '=', company_id)]"/>
</group>
</group>
<group invisible="node_btn_name != '分发'">
@@ -126,7 +125,7 @@
context="{'tree_view_ref':'yuthon_hr_employee.view_employee_phone_tree'}"/>
<field name="circulated_employee_ids" invisible="node_btn_name == '退回'"
widget="many2many_tags" options="{'no_create': True}"
domain="[('company_id', '=', company_id),('id', 'not in' , filter_personnel_ids)]"/>
domain="[('company_id', '=', company_id)]"/>
</group>
</group>
<div invisible="node_btn_name != '收回'">
@@ -149,7 +148,7 @@
</group>
<group>
<field name="circulated_employee_ids" widget="many2many_tags" options="{'no_create': True}"
domain="[('company_id', '=', company_id), ('id', 'not in' , filter_personnel_ids)] if is_this_company else [('id', 'not in' , filter_personnel_ids)]"/>
domain="[('company_id', '=', company_id)] if is_this_company else []"/>
</group>
<group>
<field name="supervising_employee_ids" widget="many2many_tags" invisible="not is_supervising"
+25
View File
@@ -286,6 +286,31 @@ class YuthonCatering(models.Model):
'lunch': True,
})
def fill_march6_missing_catering(self):
"""表3月6日缺失员工的餐饮收集任务数据"""
from datetime import date as date_cls
target_date = date_cls(2026, 3, 6)
conf_records = self.env['yuthon.catering.conf'].search([])
if not conf_records:
return
employee_ids = conf_records.mapped('employee_ids')
if not employee_ids:
return
create_vals = []
for employee in employee_ids:
existing = self.search([
('employee_id', '=', employee.id),
('date', '=', target_date),
], limit=1)
if not existing:
create_vals.append({
'date': target_date,
'employee_id': employee.id,
'state': 'unconfirmed',
})
if create_vals:
self.create(create_vals)
def batch_generate_catering(self):
"""批量生成2025-12、2026-01、2026-02的餐饮收集任务数据,排除节假日"""
from datetime import date as date_cls
@@ -13,6 +13,7 @@
<button name="task_catering22" string="一次修复" type="object" class="btn btn-primary"/>
<button name="batch_confirm_catering" string="批量确认" type="object" class="btn btn-primary"/>
<button name="batch_generate_catering" string="批量生成12/1/2月餐饮数据" type="object" class="btn btn-primary"/>
<button name="fill_march6_missing_catering" string="表3月6日缺失数据" type="object" class="btn btn-primary"/>
</header>
<field name="date"/>
<field name="breakfast"/>
+1
View File
@@ -85,6 +85,7 @@
'wizard/yuthon_run_wizard.xml',
'wizard/applet_security_quick_perm_views.xml',
'wizard/export_date_wizard_views.xml',
'wizard/overtime_change_wizard_views.xml',
'report/overtime_approval_report.xml',
'report/overtime_approval_templates.xml',
],
@@ -48,6 +48,11 @@ class YuthonVacationReports(models.Model):
late_allowed = fields.Float(related="employee_id.late_allowed", string="允许迟到次数")
leave_ids = fields.One2many('vacation.leave.line', 'vacation_id', string='年假明细')
overtime_ids = fields.One2many('vacation.over.line', 'vacation_id', string='加班明细')
# 加班明细合计字段
overtime_workday_total = fields.Float(string='工作日合计', compute='_compute_overtime_totals', digits=(16, 1))
overtime_legal_day_total = fields.Float(string='法定节假日合计', compute='_compute_overtime_totals', digits=(16, 1))
overtime_public_holidays_total = fields.Float(string='公休日合计', compute='_compute_overtime_totals', digits=(16, 1))
overtime_grand_total = fields.Float(string='总合计', compute='_compute_overtime_totals', digits=(16, 1))
meal_fee_ids = fields.One2many('vacation.meal.line', 'vacation_id', string='误餐明细')
medical_fee_ids = fields.One2many('vacation.medical.line', 'vacation_id', string='体检费明细')
parenting_line_ids = fields.One2many('vacation.parenting.line', 'vacation_id', string='育儿假明细')
@@ -200,7 +205,7 @@ class YuthonVacationReports(models.Model):
"views": [(False, "tree")],
'res_model': 'vacation.meal.line',
'domain': [
('start_time', '>=', datetime(2026, 12, 1)),
('start_time', '>=', datetime(2026, 1, 1)),
('meal', '=', 'yes'),
('vacation_id', '!=', False),
('workflow_id', '!=', False),
@@ -1408,6 +1413,49 @@ class YuthonVacationReports(models.Model):
record.statutory_leave = legal_total/8
record.cop_leave = total_add2 / 8
@api.depends('overtime_ids.workday', 'overtime_ids.legal_day', 'overtime_ids.public_holidays', 'overtime_ids.update_type', 'overtime_ids.is_void')
def _compute_overtime_totals(self):
"""计算加班明细的合计:工作日、法定节假日、公休日的合计,以及总合计
根据update_type判断是增加还是减少
"""
for record in self:
workday_total = 0.0
legal_day_total = 0.0
public_holidays_total = 0.0
for line in record.overtime_ids:
if line.is_void:
continue
if line.update_type == 'add':
workday_total += line.workday or 0
legal_day_total += line.legal_day or 0
public_holidays_total += line.public_holidays or 0
elif line.update_type == 'reduce':
workday_total -= line.workday or 0
legal_day_total -= line.legal_day or 0
public_holidays_total -= line.public_holidays or 0
record.overtime_workday_total = workday_total
record.overtime_legal_day_total = legal_day_total
record.overtime_public_holidays_total = public_holidays_total
record.overtime_grand_total = workday_total + legal_day_total + public_holidays_total
def action_overtime_change(self):
"""打开加班变更向导"""
self.ensure_one()
return {
'name': '加班变更',
'type': 'ir.actions.act_window',
'res_model': 'overtime.change.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'active_model': 'yuthon.vacation.reports',
'active_id': self.id,
},
}
@api.depends('meal_fee_ids.update_lines', 'meal_fee_ids.update_type', 'meal_fee_ids.is_void')
def _compute_meal_fee(self):
for record in self:
@@ -2274,7 +2322,7 @@ class VacationMealLine(models.Model):
duration = (end_time - start_time).total_seconds() / 3600
rec.duration = duration or ''
rec.reasons = go_approval_id.details
rec.reasons = go_approval_id.reason_application
rec.start_date = start_date
rec.start_time = start_time
rec.end_time = end_time
@@ -94,4 +94,5 @@ access_yuthon_overtime_line,yuthon.overtime.line,model_yuthon_overtime_line,base
access_overtime_active_line,overtime.active.line,model_overtime_active_line,base.group_user,1,1,1,1
access_export_date_wizard,export.date.wizard,model_export_date_wizard,base.group_user,1,1,1,1
access_vacation_meal_active,vacation.meal.active,model_vacation_meal_active,base.group_user,1,1,1,1
access_yuthon_vacation_import_wizard,yuthon.vacation.import.wizard,model_yuthon_vacation_import_wizard,base.group_user,1,1,1,1
access_yuthon_vacation_import_wizard,yuthon.vacation.import.wizard,model_yuthon_vacation_import_wizard,base.group_user,1,1,1,1
access_overtime_change_wizard,overtime.change.wizard,model_overtime_change_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
94
95
96
97
98
@@ -4,7 +4,7 @@
<field name="name">vacation.meal.line.tree</field>
<field name="model">vacation.meal.line</field>
<field name="arch" type="xml">
<tree string="误餐情况" create="false" delete="false">
<tree string="误餐情况" create="false">
<header>
<button name="confirm" type="object" string="归档" class="oe_highlight"/>
</header>
@@ -19,6 +19,7 @@
<field name="reasons"/>
<field name="meal"/>
<field name="is_filing" column_invisible="1"/>
<button name="open_record" string="详情" type="object" class="btn-primary"/>
</tree>
</field>
</record>
@@ -222,7 +223,7 @@
<notebook>
<page string="年假">
<field name="leave_ids" context="{'default_employee_id': employee_id}">
<tree editable="bottom" delete="false" decoration-muted="is_void">
<tree editable="bottom" delete="false" decoration-muted="is_void" decoration-danger="update_type == 'reduce'">
<button name="open_record" type="object" string="详情" class="btn-primary"
invisible="not workflow_record_id"/>
<field name="employee_id" column_invisible="1"/>
@@ -249,8 +250,23 @@
</page>
<page string="加班">
<button name="overtime_allocation" string="分配" type="object" class="btn btn-primary"/>
<button name="action_overtime_change" string="变更" type="object" class="btn btn-secondary"/>
<group col="4">
<group>
<field name="overtime_workday_total" readonly="1"/>
</group>
<group>
<field name="overtime_legal_day_total" readonly="1"/>
</group>
<group>
<field name="overtime_public_holidays_total" readonly="1"/>
</group>
<group>
<field name="overtime_grand_total" readonly="1"/>
</group>
</group>
<field name="overtime_ids" context="{'default_employee_id': employee_id}">
<tree editable="bottom" delete="false" decoration-muted="is_void">
<tree editable="bottom" delete="false" decoration-muted="is_void" decoration-danger="update_type == 'reduce'">
<button name="open_record" type="object" string="详情" class="btn-primary"
invisible="not workflow_record_id"/>
<field name="vacation_id" column_invisible="1"/>
+1
View File
@@ -12,3 +12,4 @@ from . import yuthon_run_wizard
from . import applet_security_quick_perm_wizard
from . import export_date_wizard
from . import yuthon_vacation_import_wizard
from . import overtime_change_wizard
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class OvertimeChangeWizard(models.TransientModel):
_name = 'overtime.change.wizard'
_description = '加班变更向导'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='假期额度记录', required=True)
employee_id = fields.Many2one('hr.employee', string='员工', related='vacation_id.employee_id', readonly=True)
# 加班明细行字段
update_type = fields.Selection([('add', '增加'), ('reduce', '减少')], string='更新类型', required=True, default='add')
update_date = fields.Date(string='更新时间', default=fields.Date.today)
workday = fields.Float(string='工作日h', digits=(16, 1), default=0.0)
legal_day = fields.Float(string='法定节假日h', digits=(16, 1), default=0.0)
public_holidays = fields.Float(string='公休日h', digits=(16, 1), default=0.0)
valid_date = fields.Date(string='有效期至')
adjust_date = fields.Date(string='调整至')
overtime_methods = fields.Selection([('leave', '补休'), ('fee', '加班费结算')], string='加班补偿方式', default='leave')
text = fields.Text(string='备注')
@api.model
def default_get(self, fields_list):
res = super(OvertimeChangeWizard, self).default_get(fields_list)
# 从上下文获取vacation_id
if self._context.get('active_model') == 'yuthon.vacation.reports' and self._context.get('active_id'):
res['vacation_id'] = self._context.get('active_id')
return res
def action_confirm(self):
"""确认按钮:创建加班明细行"""
self.ensure_one()
if not self.vacation_id:
raise ValidationError(_('未找到假期额度记录'))
# 创建加班明细行
vals = {
'vacation_id': self.vacation_id.id,
'update_type': self.update_type,
'update_date': self.update_date,
'workday': self.workday,
'legal_day': self.legal_day,
'public_holidays': self.public_holidays,
'valid_date': self.valid_date,
'adjust_date': self.adjust_date,
'overtime_methods': self.overtime_methods,
'text': self.text,
}
self.env['vacation.over.line'].create(vals)
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<!-- 加班变更向导 Form 视图 -->
<record id="view_overtime_change_wizard_form" model="ir.ui.view">
<field name="name">overtime.change.wizard.form</field>
<field name="model">overtime.change.wizard</field>
<field name="arch" type="xml">
<form string="加班变更">
<sheet>
<group>
<group>
<field name="employee_id" readonly="1"/>
<field name="update_type"/>
<field name="update_date"/>
</group>
<group>
<field name="workday"/>
<field name="legal_day"/>
<field name="public_holidays"/>
</group>
</group>
<group>
<group>
<field name="valid_date"/>
<field name="adjust_date"/>
</group>
<group>
<field name="overtime_methods"/>
</group>
</group>
<group>
<field name="text"/>
</group>
</sheet>
<footer>
<button name="action_confirm" string="确认" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- 加班变更向导 Action -->
<record id="action_overtime_change_wizard" model="ir.actions.act_window">
<field name="name">加班变更</field>
<field name="res_model">overtime.change.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>
@@ -49,19 +49,44 @@ class YuthonVacationImportWizard(models.TransientModel):
target_leaves = vacation_id.leave_ids.filtered(lambda l: l.text == '年假分配')
for leave in target_leaves:
update_value = float(row[1].value) * 8
if update_value == 0:
leave.unlink()
else:
leave.write({'update_lines': update_value})
leave.write({'update_lines': update_value})
target_leaves1 = vacation_id.leave_ids.filtered(lambda l: l.text == '上月额度继承')
update_value = float(row[2].value) * 8
for leave in target_leaves1:
if update_value == 0:
leave.unlink()
else:
leave.write({'update_lines': update_value})
leave.write({'update_lines': update_value})
if not target_leaves1 and row[2].value:
if employee_id.company_id.name == '天河粮储':
valid_date = datetime(2026, 6, 30)
else:
valid_date = None
self.env['vacation.leave.line'].create({
'text': '上月额度继承',
'update_lines': float(row[2].value) * 8,
'vacation_id': vacation_id.id,
'update_type': 'add',
'employee_id': employee_id.id,
'update_date': date,
'valid_date': valid_date,
})
if not target_leaves and row[1].value:
if employee_id.company_id.name == '天河粮储':
valid_date = datetime(2027, 6, 30)
else:
valid_date = None
self.env['vacation.leave.line'].create({
'text': '年假分配',
'employee_id': employee_id.id,
'update_type': 'add',
'update_lines': float(row[1].value) * 8,
'vacation_id': vacation_id.id,
'update_date': date,
'valid_date': valid_date,
})
target_overtimes = vacation_id.overtime_ids.filtered(lambda o: o.text == '上月额度继承')
if employee_id.company_id.name == '集团本部':
workday_value = float(row[3].value)