diff --git a/yuthon_hr_employee/__manifest__.py b/yuthon_hr_employee/__manifest__.py
index fd3ff90fe..2536ebbb0 100644
--- a/yuthon_hr_employee/__manifest__.py
+++ b/yuthon_hr_employee/__manifest__.py
@@ -40,6 +40,7 @@
'wizard/hr_job_add_wizard_views.xml',
'wizard/medical_manage_wizard_viwes.xml',
'wizard/reset_password_wizard_viwes.xml',
+ 'views/yuthon_hr_task_views.xml',
],
'assets': {
'web.assets_backend': [
diff --git a/yuthon_hr_employee/data/abnormal_time_data.xml b/yuthon_hr_employee/data/abnormal_time_data.xml
index 13a12a4ad..c6945016e 100644
--- a/yuthon_hr_employee/data/abnormal_time_data.xml
+++ b/yuthon_hr_employee/data/abnormal_time_data.xml
@@ -68,5 +68,31 @@
-1
+
+ 定时任务:退休提醒
+
+ code
+ model.task_retire_reminder()
+
+
+ 1
+ days
+
+ -1
+
+
+
+ 定时任务:离职提醒
+
+ code
+ model.task_separation_reminder()
+
+
+ 1
+ days
+
+ -1
+
+
diff --git a/yuthon_hr_employee/models/__init__.py b/yuthon_hr_employee/models/__init__.py
index 7fb0850f7..400f869be 100644
--- a/yuthon_hr_employee/models/__init__.py
+++ b/yuthon_hr_employee/models/__init__.py
@@ -14,4 +14,5 @@ from . import soong_user_role
from . import yuthon_medical_manage
from . import yuthon_medical_rule
from . import res_users
+from . import yuthon_hr_task
diff --git a/yuthon_hr_employee/models/yuthon_hr_employee.py b/yuthon_hr_employee/models/yuthon_hr_employee.py
index de14e4843..39988e3ac 100644
--- a/yuthon_hr_employee/models/yuthon_hr_employee.py
+++ b/yuthon_hr_employee/models/yuthon_hr_employee.py
@@ -1230,3 +1230,107 @@ class YuthonHrEmployee(models.Model):
url=url, btntxt="详细信息")
employee.write({'is_create': True})
+
+ def _get_notify_users(self, task_config):
+ """从 task_config 获取需要通知的用户列表.
+ """
+ user_ids = set(task_config.user_ids.ids)
+ role_ids = set(task_config.users_role_group_ids.mapped('user_role_ids').ids)
+ if task_config.users_role_id:
+ role_ids.add(task_config.users_role_id.id)
+ if role_ids:
+ all_target_roles = self.env['soong.user.role'].browse(list(role_ids))
+ role_employees = self.env['hr.employee'].search([
+ '|',
+ ('user_role_id', 'in', all_target_roles.ids),
+ ('user_role_ids', 'in', all_target_roles.ids),
+ ('user_id', '!=', False),
+ ])
+ for emp in role_employees:
+ user_ids.add(emp.user_id.id)
+ return self.env['res.users'].browse(list(user_ids))
+
+ def task_retire_reminder(self):
+ today = fields.Date.today()
+ target_date = today + timedelta(days=30)
+ employees = self.search([
+ ('retire_date', '=', target_date),
+ ('user_state', 'in', ['in', 'borrow', 'expatriate', 'adjunct']),
+ ])
+ if not employees:
+ return
+ task_config = self.env['yuthon.hr.task'].search([('cron_id.name', '=', '定时任务:退休提醒')], limit=1)
+ if not task_config:
+ return
+ notify_users = self._get_notify_users(task_config)
+ if not notify_users:
+ return
+ count = len(employees)
+ for user in notify_users:
+ hr_employee = user.employee_ids.filtered(lambda e: e.wecom_userid)[:1]
+ if not hr_employee:
+ continue
+ result_data = self.sudo().env['wecom.apps'].sync_send_message_textcard(
+ category='oa',
+ user_id=hr_employee.wecom_userid,
+ title='退休提醒',
+ description=f"提醒:有{count}个人即将退休,请移至工作台查看。",
+ url='https://oa.thtzjt.com/web?debug=1#action=3584&cids=1&menu_id=9668',
+ btntxt='详细信息',
+ )
+ confirm = self.env['yuthon.task.confirm'].create({
+ 'task_id': task_config.id,
+ 'user_id': user.id,
+ 'state': 'read',
+ })
+ if result_data and len(result_data) > 1:
+ msg_id = result_data[1].get('msgid')
+ if msg_id:
+ confirm.sudo().write({'msg_id': msg_id})
+
+ def task_separation_reminder(self):
+ today = fields.Date.today()
+ target_date = today + timedelta(days=30)
+ workflow_records = self.env['soong.workflow.record'].search([
+ ('workflow_id.name', '=', '离职申请'),
+ ('approve_state', '=', 'end'),
+ ('reclaim_state', '=', False),
+ ])
+ leaving_ids = []
+ for record in workflow_records:
+ try:
+ leaving = self.env[record.res_model].browse(record.res_id)
+ if leaving and leaving.separation_date == target_date and not leaving.is_execution:
+ leaving_ids.append(leaving)
+ except Exception:
+ continue
+ if not leaving_ids:
+ return
+ task_config = self.env['yuthon.hr.task'].search([('cron_id.name', '=', '定时任务:离职提醒')], limit=1)
+ if not task_config:
+ return
+ notify_users = self._get_notify_users(task_config)
+ if not notify_users:
+ return
+ count = len(leaving_ids)
+ for user in notify_users:
+ hr_employee = user.employee_ids.filtered(lambda e: e.wecom_userid)[:1]
+ if not hr_employee:
+ continue
+ result_data = self.sudo().env['wecom.apps'].sync_send_message_textcard(
+ category='oa',
+ user_id=hr_employee.wecom_userid,
+ title='离职提醒',
+ description=f"提醒:有{count}个人即将离职,请移至工作台查看。",
+ url='https://oa.thtzjt.com/web?debug=1#action=3584&cids=1&menu_id=9668',
+ btntxt='详细信息',
+ )
+ confirm = self.env['yuthon.task.confirm'].create({
+ 'task_id': task_config.id,
+ 'user_id': user.id,
+ 'state': 'read',
+ })
+ if result_data and len(result_data) > 1:
+ msg_id = result_data[1].get('msgid')
+ if msg_id:
+ confirm.sudo().write({'msg_id': msg_id})
diff --git a/yuthon_hr_employee/security/ir.model.access.csv b/yuthon_hr_employee/security/ir.model.access.csv
index 11eb7fb05..57c27c481 100644
--- a/yuthon_hr_employee/security/ir.model.access.csv
+++ b/yuthon_hr_employee/security/ir.model.access.csv
@@ -24,4 +24,6 @@ access_child_care_line,child_care_line,model_child_care_line,base.group_user,1,1
access_child_education_line,child_education_line,model_child_education_line,base.group_user,1,1,1,1
access_elderly_support_line,elderly_support_line,model_elderly_support_line,base.group_user,1,1,1,1
access_reset_password_wizard, reset_password_wizard,model_reset_password_wizard,base.group_user,1,1,1,1
-access_medical_manage_wizard, medical_manage_wizard,model_medical_manage_wizard,base.group_user,1,1,1,1
\ No newline at end of file
+access_medical_manage_wizard, medical_manage_wizard,model_medical_manage_wizard,base.group_user,1,1,1,1
+access_yuthon_hr_task,yuthon_hr_task,model_yuthon_hr_task,base.group_user,1,1,1,1
+access_yuthon_task_confirm,yuthon_task_confirm,model_yuthon_task_confirm,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/yuthon_property/models/property_bank_statements.py b/yuthon_property/models/property_bank_statements.py
index c1238f551..489ca525b 100644
--- a/yuthon_property/models/property_bank_statements.py
+++ b/yuthon_property/models/property_bank_statements.py
@@ -255,6 +255,7 @@ class ReceivableModifyLine(models.Model):
continue
all_lines = self.search([('receivable_id', '=', rec.receivable_id.id)])
+ all_lines |= rec # 确保当前记录包含在内(处理新建未保存的记录)
sorted_lines = all_lines.sorted(
key=lambda l: (
0 if l.reconciliation_date else 1,
@@ -270,6 +271,9 @@ class ReceivableModifyLine(models.Model):
rec.delivered1 = rec.receivable_id.original_amount if remaining <= 0 else remaining
break
cumsum += line.amount
+ else:
+ # 兜底:确保 delivered1 总是被赋值
+ rec.delivered1 = 0
remake = fields.Text(related='receivable_id.remark', string='备注', store=False)
# lessee_id = fields.Many2one(
diff --git a/yuthon_property_approval/__manifest__.py b/yuthon_property_approval/__manifest__.py
index 4bea54a1c..d2e448b48 100644
--- a/yuthon_property_approval/__manifest__.py
+++ b/yuthon_property_approval/__manifest__.py
@@ -88,6 +88,7 @@
'views/property_delegated_agent_views.xml',
'views/property_transaction_confirmed.xml',
'views/property_meeting_approval_views.xml',
+ 'views/yuthon_property_task_views.xml',
'views/receivable_modify_line_views.xml',
'wizard/property_add_wizard_views.xml',
'wizard/property_contract_wizard.xml',
diff --git a/yuthon_property_approval/data/property_contract_time.xml b/yuthon_property_approval/data/property_contract_time.xml
index 2901db9e3..6869c9a67 100644
--- a/yuthon_property_approval/data/property_contract_time.xml
+++ b/yuthon_property_approval/data/property_contract_time.xml
@@ -40,4 +40,30 @@
-1
+
+ 定时任务:物业空置提醒
+
+ code
+ model.task_property_vacant_reminder()
+
+
+ 1
+ days
+
+ -1
+
+
+
+ 定时任务:物业合同到期提醒
+
+ code
+ model.task_property_contract_reminder()
+
+
+ 1
+ days
+
+ -1
+
+
\ No newline at end of file
diff --git a/yuthon_property_approval/models/__init__.py b/yuthon_property_approval/models/__init__.py
index 1b98f6ae4..51944a381 100644
--- a/yuthon_property_approval/models/__init__.py
+++ b/yuthon_property_approval/models/__init__.py
@@ -63,4 +63,5 @@ from . import property_payment_notice
from . import property_bills_list
from . import property_fee_spending
from . import property_fee_form
-from . import receivable_modify_line
\ No newline at end of file
+from . import receivable_modify_line
+from . import yuthon_property_task
diff --git a/yuthon_property_approval/models/property_lease_contract.py b/yuthon_property_approval/models/property_lease_contract.py
index e38895f93..983bb6000 100644
--- a/yuthon_property_approval/models/property_lease_contract.py
+++ b/yuthon_property_approval/models/property_lease_contract.py
@@ -2547,6 +2547,124 @@ class PropertyLeaseContract(models.Model):
report_ref = self.env.ref('yuthon_property_approval.action_property_lease_contract_print_report')
return report_ref.report_action(self)
+ def _get_property_notify_users(self, task_config):
+ """从 task_config 获取需要通知的用户列表.
+ - user_ids: 直接指定的人
+ - users_role_id + users_role_group_ids: 角色组 -> 角色 -> 花名册中 user_role_id(主角色)或 user_role_ids(辅助角色)匹配的员工
+ """
+ user_ids = set(task_config.user_ids.ids)
+ role_ids = set(task_config.users_role_group_ids.mapped('user_role_ids').ids)
+ if task_config.users_role_id:
+ role_ids.add(task_config.users_role_id.id)
+ if role_ids:
+ all_target_roles = self.env['soong.user.role'].browse(list(role_ids))
+ role_employees = self.env['hr.employee'].search([
+ '|',
+ ('user_role_id', 'in', all_target_roles.ids),
+ ('user_role_ids', 'in', all_target_roles.ids),
+ ('user_id', '!=', False),
+ ])
+ for emp in role_employees:
+ user_ids.add(emp.user_id.id)
+ return self.env['res.users'].browse(list(user_ids))
+
+ def task_property_vacant_reminder(self):
+ """物业空置提醒:以物业为单位,合同实际结束日期距今已3个月或以上,或没有合同."""
+ today = fields.Date.today()
+ three_months_ago = today - timedelta(days=90)
+ properties = self.env['yuthon.property'].search([])
+ vacant_properties = self.env['yuthon.property']
+ for prop in properties:
+ contract_lines = self.env['property.lease.contract.line'].search([
+ ('address_id', '=', prop.id),
+ ])
+ if not contract_lines:
+ vacant_properties |= prop
+ continue
+ contracts = contract_lines.mapped('lease_id')
+ active_contracts = contracts.filtered(
+ lambda c: c.contact_state not in ['complete', 'terminate', 'early_termination', 'settle']
+ )
+ if active_contracts:
+ continue
+ ended_contracts = contracts.filtered(
+ lambda c: c.contact_state in ['complete', 'terminate', 'early_termination', 'settle'] and c.actual_end_time
+ )
+ if not ended_contracts:
+ continue
+ last_end_time = max(ended_contracts.mapped('actual_end_time'))
+ if last_end_time <= three_months_ago:
+ vacant_properties |= prop
+ if not vacant_properties:
+ return
+ task_config = self.env['yuthon.property.task'].search([('cron_id.name', '=', '定时任务:物业空置提醒')], limit=1)
+ if not task_config:
+ return
+ notify_users = self._get_property_notify_users(task_config)
+ if not notify_users:
+ return
+ count = len(vacant_properties)
+ for user in notify_users:
+ hr_employee = user.employee_ids.filtered(lambda e: e.wecom_userid)[:1]
+ if not hr_employee:
+ continue
+ result_data = self.sudo().env['wecom.apps'].sync_send_message_textcard(
+ category='oa',
+ user_id=hr_employee.wecom_userid,
+ title='物业空置提醒',
+ description=f"提醒:有{count}个物业已空置,请移至工作台查看。",
+ url='https://oa.thtzjt.com/web?debug=1#action=3584&cids=1&menu_id=9668',
+ btntxt='详细信息',
+ )
+ confirm = self.env['yuthon.property.task.confirm'].create({
+ 'task_id': task_config.id,
+ 'user_id': user.id,
+ 'state': 'read',
+ })
+ if result_data and len(result_data) > 1:
+ msg_id = result_data[1].get('msgid')
+ if msg_id:
+ confirm.sudo().write({'msg_id': msg_id})
+
+ def task_property_contract_reminder(self):
+ """物业合同到期提醒:距实际结束日期还有3个月."""
+ today = fields.Date.today()
+ target_date = today + timedelta(days=90)
+ contracts = self.search([
+ ('actual_end_time', '=', target_date),
+ ('contact_state', 'in', ['to', 'ing']),
+ ])
+ if not contracts:
+ return
+ task_config = self.env['yuthon.property.task'].search([('cron_id.name', '=', '定时任务:物业合同到期提醒')], limit=1)
+ if not task_config:
+ return
+ notify_users = self._get_property_notify_users(task_config)
+ if not notify_users:
+ return
+ count = len(contracts)
+ for user in notify_users:
+ hr_employee = user.employee_ids.filtered(lambda e: e.wecom_userid)[:1]
+ if not hr_employee:
+ continue
+ result_data = self.sudo().env['wecom.apps'].sync_send_message_textcard(
+ category='oa',
+ user_id=hr_employee.wecom_userid,
+ title='物业合同到期提醒',
+ description=f"提醒:有{count}个物业即将到期,请移至工作台查看。",
+ url='https://oa.thtzjt.com/web?debug=1#action=3584&cids=1&menu_id=9668',
+ btntxt='详细信息',
+ )
+ confirm = self.env['yuthon.property.task.confirm'].create({
+ 'task_id': task_config.id,
+ 'user_id': user.id,
+ 'state': 'read',
+ })
+ if result_data and len(result_data) > 1:
+ msg_id = result_data[1].get('msgid')
+ if msg_id:
+ confirm.sudo().write({'msg_id': msg_id})
+
class PropertyLeaseContractLine(models.Model):
_name = 'property.lease.contract.line'
_description = '物业列表明细行'
diff --git a/yuthon_property_approval/security/ir.model.access.csv b/yuthon_property_approval/security/ir.model.access.csv
index 44684b12c..2b242369f 100644
--- a/yuthon_property_approval/security/ir.model.access.csv
+++ b/yuthon_property_approval/security/ir.model.access.csv
@@ -143,4 +143,6 @@ access_refund_wizard,refund_wizard,model_refund_wizard,base.group_user,1,1,1,1
access_refund_wizard_line,refund_wizard_line,model_refund_wizard_line,base.group_user,1,1,1,1
access_property_business_type,property_business_type,model_property_business_type,base.group_user,1,1,1,1
access_property_payment_type,property_payment_type,model_property_payment_type,base.group_user,1,1,1,1
-access_property_contract_rental_wizard,property_contract_rental_wizard,model_property_contract_rental_wizard,base.group_user,1,1,1,1
\ No newline at end of file
+access_property_contract_rental_wizard,property_contract_rental_wizard,model_property_contract_rental_wizard,base.group_user,1,1,1,1
+access_yuthon_property_task,yuthon_property_task,model_yuthon_property_task,base.group_user,1,1,1,1
+access_yuthon_property_task_confirm,yuthon_property_task_confirm,model_yuthon_property_task_confirm,base.group_user,1,1,1,1
\ No newline at end of file