From 1fb7ae189c997f76be67fc9fa7d4a76471cfe28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= <9664676+pengyuthon@user.noreply.gitee.com> Date: Sat, 25 Jul 2026 14:23:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_hr_employee/__init__.py | 3 + yuthon_hr_employee/__manifest__.py | 34 ++ yuthon_hr_employee/controllers/__init__.py | 1 + .../controllers/employee_banner_route.py | 29 + .../data/employee_code_data.xml | 9 + yuthon_hr_employee/models/__init__.py | 4 + yuthon_hr_employee/models/hr_employee_page.py | 66 +++ yuthon_hr_employee/models/inherit_hr_job.py | 52 ++ .../models/inherit_res_groups.py | 63 +++ yuthon_hr_employee/models/inherit_res_user.py | 56 ++ .../models/yuthon_hr_employee.py | 373 +++++++++++++ .../models/yuthon_hr_employee_pages.py | 61 ++ .../security/ir.model.access.csv | 8 + .../static/description/icon.png | Bin 0 -> 6522 bytes .../static/description/icon.svg | 1 + .../static/others/user_template.xlsx | Bin 0 -> 12829 bytes .../static/scss/yuthon_employee_scss.scss | 13 + .../views/inherit_hr_job_views.xml | 206 +++++++ .../views/inherit_hr_work_location_views.xml | 17 + .../views/inherit_res_groups_views.xml | 28 + .../views/inherit_res_user_views.xml | 29 + .../views/yuthon_hr_employee_views.xml | 524 ++++++++++++++++++ .../views/yuthon_inherit_hr_employee.xml | 131 +++++ yuthon_hr_employee/wizard/__init__.py | 3 + .../wizard/hr_employee_time_wizard.py | 34 ++ .../wizard/hr_employee_time_wizard_views.xml | 22 + .../wizard/hr_employee_wizard.py | 369 ++++++++++++ .../wizard/hr_employee_wizard_views.xml | 34 ++ .../wizard/reset_password_wizard.py | 15 + .../wizard/reset_password_wizard_viwes.xml | 21 + yuthon_notice/__init__.py | 3 +- yuthon_notice/__manifest__.py | 11 +- yuthon_notice/models/__init__.py | 1 + yuthon_notice/models/hr_department.py | 19 + yuthon_notice/models/yuthon_confirm_users.py | 82 ++- yuthon_notice/models/yuthon_notice.py | 312 +++++++++-- yuthon_notice/report/notice_print_report.xml | 23 + .../report/notice_print_template.xml | 144 +++++ yuthon_notice/static/scss/notice_scss.scss | 4 + .../views/yuthon_confirm_users_views.xml | 24 +- yuthon_notice/views/yuthon_cs_tree_views.xml | 6 +- .../views/yuthon_notice_type_views.xml | 4 +- yuthon_notice/views/yuthon_notice_views.xml | 88 ++- yuthon_will_task/__init__.py | 2 +- yuthon_will_task/__manifest__.py | 11 +- yuthon_will_task/controllers/__init__.py | 2 +- yuthon_will_task/controllers/main.py | 346 +++++------- yuthon_will_task/data/tsak_time_data.xml | 2 +- yuthon_will_task/models/__init__.py | 1 + .../models/inherit_room_booking.py | 116 ++++ yuthon_will_task/models/yuthon_dates_task.py | 11 +- yuthon_will_task/models/yuthon_task_time.py | 1 - yuthon_will_task/models/yuthon_will_task.py | 209 ++++--- yuthon_will_task/security/ir.model.access.csv | 11 +- .../security/yuthon_will_task_security.xml | 10 +- .../static/src/css/task_detail.css | 105 ++++ .../static/src/js/task_create_button.js | 12 + .../static/src/xml/task_create_button.xml | 3 + yuthon_will_task/views/task_template.xml | 70 +++ .../views/yuthon_dates_task_views.xml | 12 +- yuthon_will_task/views/yuthon_task_menu.xml | 18 +- .../views/yuthon_task_time_views.xml | 12 +- .../views/yuthon_will_task_views.xml | 63 ++- 63 files changed, 3444 insertions(+), 500 deletions(-) create mode 100644 yuthon_hr_employee/__init__.py create mode 100644 yuthon_hr_employee/__manifest__.py create mode 100644 yuthon_hr_employee/controllers/__init__.py create mode 100644 yuthon_hr_employee/controllers/employee_banner_route.py create mode 100644 yuthon_hr_employee/data/employee_code_data.xml create mode 100644 yuthon_hr_employee/models/__init__.py create mode 100644 yuthon_hr_employee/models/hr_employee_page.py create mode 100644 yuthon_hr_employee/models/inherit_hr_job.py create mode 100644 yuthon_hr_employee/models/inherit_res_groups.py create mode 100644 yuthon_hr_employee/models/inherit_res_user.py create mode 100644 yuthon_hr_employee/models/yuthon_hr_employee.py create mode 100644 yuthon_hr_employee/models/yuthon_hr_employee_pages.py create mode 100644 yuthon_hr_employee/security/ir.model.access.csv create mode 100644 yuthon_hr_employee/static/description/icon.png create mode 100644 yuthon_hr_employee/static/description/icon.svg create mode 100644 yuthon_hr_employee/static/others/user_template.xlsx create mode 100644 yuthon_hr_employee/static/scss/yuthon_employee_scss.scss create mode 100644 yuthon_hr_employee/views/inherit_hr_job_views.xml create mode 100644 yuthon_hr_employee/views/inherit_hr_work_location_views.xml create mode 100644 yuthon_hr_employee/views/inherit_res_groups_views.xml create mode 100644 yuthon_hr_employee/views/inherit_res_user_views.xml create mode 100644 yuthon_hr_employee/views/yuthon_hr_employee_views.xml create mode 100644 yuthon_hr_employee/views/yuthon_inherit_hr_employee.xml create mode 100644 yuthon_hr_employee/wizard/__init__.py create mode 100644 yuthon_hr_employee/wizard/hr_employee_time_wizard.py create mode 100644 yuthon_hr_employee/wizard/hr_employee_time_wizard_views.xml create mode 100644 yuthon_hr_employee/wizard/hr_employee_wizard.py create mode 100644 yuthon_hr_employee/wizard/hr_employee_wizard_views.xml create mode 100644 yuthon_hr_employee/wizard/reset_password_wizard.py create mode 100644 yuthon_hr_employee/wizard/reset_password_wizard_viwes.xml create mode 100644 yuthon_notice/models/hr_department.py create mode 100644 yuthon_notice/report/notice_print_report.xml create mode 100644 yuthon_notice/report/notice_print_template.xml create mode 100644 yuthon_will_task/models/inherit_room_booking.py create mode 100644 yuthon_will_task/static/src/css/task_detail.css create mode 100644 yuthon_will_task/views/task_template.xml diff --git a/yuthon_hr_employee/__init__.py b/yuthon_hr_employee/__init__.py new file mode 100644 index 0000000..e4f4917 --- /dev/null +++ b/yuthon_hr_employee/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import models +from . import wizard diff --git a/yuthon_hr_employee/__manifest__.py b/yuthon_hr_employee/__manifest__.py new file mode 100644 index 0000000..97b94f8 --- /dev/null +++ b/yuthon_hr_employee/__manifest__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# email:pengyuthon@163.com + +{ + 'name': 'Yuthon Hr', + 'summary': '', + 'description': ''' + ''', + 'version': '18.0.2.0.0', + 'category': 'Human Resources/Employees', + 'license': 'LGPL-3', + 'author': 'pengyuthon', + 'website': 'https://www.pengyuthon.com', + 'depends': ['base', 'hr'], + 'data': [ + 'data/employee_code_data.xml', + 'security/ir.model.access.csv', + 'views/yuthon_hr_employee_views.xml', + 'views/inherit_res_user_views.xml', + 'views/inherit_hr_work_location_views.xml', + 'views/inherit_res_groups_views.xml', + 'wizard/hr_employee_wizard_views.xml', + 'wizard/hr_employee_time_wizard_views.xml', + 'wizard/reset_password_wizard_viwes.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'yuthon_hr_employee/static/scss/yuthon_employee_scss.scss', + ], + }, + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/yuthon_hr_employee/controllers/__init__.py b/yuthon_hr_employee/controllers/__init__.py new file mode 100644 index 0000000..c1b47dc --- /dev/null +++ b/yuthon_hr_employee/controllers/__init__.py @@ -0,0 +1 @@ +from . import employee_banner_route diff --git a/yuthon_hr_employee/controllers/employee_banner_route.py b/yuthon_hr_employee/controllers/employee_banner_route.py new file mode 100644 index 0000000..4861c73 --- /dev/null +++ b/yuthon_hr_employee/controllers/employee_banner_route.py @@ -0,0 +1,29 @@ +from odoo.http import Controller, request, route + + +class EmployeeBannerRoute(Controller): + + @route('/employee/banner/route', type='json', auth='user') + def employee_banner_route(self, **kwargs): + employees = request.env['hr.employee'].sudo() + hired_count = employees.search_count([('use_user_type', '=', 'formal_one')]) # 正式 + in_hired_count = employees.search_count([]) # 在职 + all_hired_count = employees.search_count([]) # 全职 + return { + # 'html': f""" + #
+ #
+ #

正式

+ #

{hired_count}

+ #
+ #
+ #

在职

+ #

{in_hired_count}

+ #
+ #
+ #

全职

+ #

{all_hired_count}

+ #
+ #
+ # """ + } diff --git a/yuthon_hr_employee/data/employee_code_data.xml b/yuthon_hr_employee/data/employee_code_data.xml new file mode 100644 index 0000000..d659943 --- /dev/null +++ b/yuthon_hr_employee/data/employee_code_data.xml @@ -0,0 +1,9 @@ + + + + 员工编号 + employee_code + US%(y)s%(month)s%(day)s + 3 + + diff --git a/yuthon_hr_employee/models/__init__.py b/yuthon_hr_employee/models/__init__.py new file mode 100644 index 0000000..e9ea09a --- /dev/null +++ b/yuthon_hr_employee/models/__init__.py @@ -0,0 +1,4 @@ +from . import yuthon_hr_employee +from . import hr_employee_page +from . import inherit_res_user +from . import inherit_res_groups diff --git a/yuthon_hr_employee/models/hr_employee_page.py b/yuthon_hr_employee/models/hr_employee_page.py new file mode 100644 index 0000000..ba06a59 --- /dev/null +++ b/yuthon_hr_employee/models/hr_employee_page.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, _ + + +class YuthonEmergencyContactLine(models.Model): + _name = 'yuthon.emergency.contact.line' + _description = "紧急联系人明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + name = fields.Char(string="姓名") + user_sfz = fields.Char(string="身份证") + user_relationship = fields.Selection([("father", "父亲"),('mother', '母亲'), ("spouse", "妻子"), ('boys', '儿子'), ('daughter', '女儿'), ('didi', '弟弟'), + ('husband', '丈夫'), ('sister', '姐姐'), ('brother', '哥哥'), ('sister_do', '妹妹')], string="关系") + workplace = fields.Char(string="工作单位") + post = fields.Char(string="岗位") + phone_number = fields.Char(string="手机号") + home_address = fields.Char(string="家庭住址") + documents_ids = fields.Many2many('ir.attachment', string="附件") + + +class YuthonWorkExperienceLine(models.Model): + _name = 'yuthon.work.experience.line' + _description = "工作经验明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + start_date = fields.Date(string="开始日期") + end_date = fields.Date(string="结束日期") + work_place = fields.Char(string="工作单位") + post = fields.Char(string="岗位") + contact_user = fields.Char(string="联系人") + phone_number = fields.Char(string="联系人手机号") + reason_leaving = fields.Char(string="离职原因") + + +class YuthonEducationExperienceLine(models.Model): + _name = 'yuthon.education.experience.line' + _description = "教育经历明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + name_school = fields.Char(string="学校名称") + start_date = fields.Date(string="入学时间") + end_date = fields.Date(string="毕业时间") + in_school_duties = fields.Char(string="在校职务") + professional = fields.Char(string="专业") + learning_style = fields.Selection([ + ('full_time', '全日制'), + ('upgrade', '专升本'), + ('adult_education', '成人教育'), + ('self_study', '自考'), + ('online_education', '网络教育'), + ('correspondence', '函授'), + ], string="学习方式") + part_time_degree = fields.Char(string="学历") + full_time_degree = fields.Char(string="学位") + documents_ids = fields.Many2many('ir.attachment', string="附件") + + +class YuthonSkillsCertificatesLine(models.Model): + _name = 'yuthon.skills.certificates.line' + _description = "技能证书管理明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + rank_name = fields.Char(string='职级名称') + rank_type = fields.Char(string='职级类型') + rank_time = fields.Date(string='职级时间') + documents_ids = fields.Many2many('ir.attachment', string="附件") diff --git a/yuthon_hr_employee/models/inherit_hr_job.py b/yuthon_hr_employee/models/inherit_hr_job.py new file mode 100644 index 0000000..40126cf --- /dev/null +++ b/yuthon_hr_employee/models/inherit_hr_job.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, _ + + +class HrJob(models.Model): + _inherit = "hr.job" + _order = 'number asc' + + number = fields.Char(string="序号") + job_type_id = fields.Many2one('hr.job.type', string="岗位类别") + start_date = fields.Date(string="生效日期", default=lambda self: fields.Date.today()) + end_date = fields.Date(string="失效日期", default=fields.Date.from_string('2099-01-01')) + is_enable = fields.Boolean(string="是否启用", default=False, store=True) + compiling_external = fields.Integer(string="编制外人数") + compiling_status = fields.Selection([('1', '缺编'), ('2', '满编')], string="状态") + missing_number = fields.Integer(string="缺编人数") + establishment_quotas = fields.Integer(string="编制定员") + job_personnel_ids = fields.Many2many('hr.employee', string='在岗人员') + recruitment_employees = fields.Integer(string="招聘人数") + email = fields.Char(string="邮箱") + job_personnel = fields.Text(string="在岗人员") + remarks = fields.Text(string="备注") + is_recruitment_status = fields.Boolean(string="招聘状态") + company_id = fields.Many2one('res.company', string='公司') + is_published = fields.Boolean(string="已发布") + website = fields.Char(string="网站") + skill_ids = fields.Many2many('hr.skill', string="技能") + date_from = fields.Date(string="开始日期") + + #任职要求 + degree = fields.Selection([('high', '高中'), ('junior_college', '大专'), ('bachelor', '本科'), + ('master', '硕士'), ('doctor', '博士')], string="学历") + sex = fields.Selection([('boy', '男'), ('girl', '女'), ('other', '不限')], string="性别") + age_start = fields.Integer(string="年龄") + age_end = fields.Integer(string="年龄") + political_aspects = fields.Selection([('cpc_dang', '中共党员'), ('nld', '民盟'), + ('league_member', '共青团员'), ('people', '群众')], string="政治面貌") + professional = fields.Char(string="专业") + job_title = fields.Char(string="职称") + admissions = fields.Char(string="执业资格") + years_of_service = fields.Integer(string="工作年限") + competency_requirements = fields.Text(string="岗位职责") + other_notes = fields.Text(string="任职要求") + function = fields.Text(string="岗位职责") + department_ids = fields.Many2one('hr.department', string="所属部门") + recruitment_target1 = fields.Selection([('new', '新员工'), ('transfer', '内部调动')], string="招聘目标") + address_id = fields.Many2one('res.partner', string="工作地点") + industry_id = fields.Many2one('res.partner', string="行业") + recruit_employee_id = fields.Many2one('hr.employee', string="招聘负责人") + interviewer_employee_id = fields.Many2one('hr.employee', string="面试官") + + diff --git a/yuthon_hr_employee/models/inherit_res_groups.py b/yuthon_hr_employee/models/inherit_res_groups.py new file mode 100644 index 0000000..d4104e0 --- /dev/null +++ b/yuthon_hr_employee/models/inherit_res_groups.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, _ +import datetime + + +class ResGroups(models.Model): + _inherit = "res.groups" + + users_ids = fields.Many2many('res.users', string="权限用户") + employee_ids = fields.Many2many('hr.employee', string="权限员工") + department_ids = fields.Many2many("hr.department", string="权限部门") + + def users_unlink(self): + users_to_unlink = self.users.filtered(lambda user: user.name != 'Administrator') + users_to_unlink2 = self.users.filtered(lambda user: user.name != '叶华峰') + self.write({'users': [(3, user.id) for user in users_to_unlink]}) + self.write({'users': [(3, user.id) for user in users_to_unlink2]}) + + def domain_unlink(self): + self.write({'users_ids': [(5, 0, 0)]}) + self.write({'employee_ids': [(5, 0, 0)]}) + self.write({'department_ids': [(5, 0, 0)]}) + + def rule_unlink(self): + self.rule_groups.unlink() + self.model_access.unlink() + + @api.onchange('employee_ids', 'department_ids') + def _onchange_update_users(self): + """动态更新 users 字段,基于员工、部门的选择""" + all_user_ids = set() + + # 1. 添加选中员工对应的用户 + for employee in self.employee_ids: + if employee.user_id: + all_user_ids.add(employee.user_id.id) + + # 2. 添加选中部门及其子部门的所有员工对应的用户 + def get_department_employees(department): + valid_user_ids = set() + if not department: + return valid_user_ids + + # 处理虚拟记录(onchange时可能出现) + dept_id = department.id.origin if hasattr(department.id, 'origin') else department.id + dept_id = int(dept_id) if isinstance(dept_id, str) else dept_id + + # 当前部门员工 + employees = self.env['hr.employee'].search([('department_id', '=', dept_id)]) + valid_user_ids.update([emp.user_id.id for emp in employees if emp.user_id]) + + # 递归子部门 + child_depts = self.env['hr.department'].search([('parent_id', '=', dept_id)]) + for child in child_depts: + valid_user_ids.update(get_department_employees(child)) + + return valid_user_ids + + for department in self.department_ids: + all_user_ids.update(get_department_employees(department)) + + # 最终更新用户 + self.users = [(6, 0, list(all_user_ids))] diff --git a/yuthon_hr_employee/models/inherit_res_user.py b/yuthon_hr_employee/models/inherit_res_user.py new file mode 100644 index 0000000..ff289c3 --- /dev/null +++ b/yuthon_hr_employee/models/inherit_res_user.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +import pypinyin + + +class ResUsers(models.Model): + _inherit = "res.users" + + # access_token2 = fields.Char(string='OAuth 访问令牌2', help='验证消息通知。') + company_ids1 = fields.Many2many('res.company', string='公司', compute='_compute_company_ids1', store=True) + @api.depends("company_ids") + def _compute_company_ids1(self): + for record in self: + employee_id = record.env['hr.employee'].search([('user_id', '=', record.id)], limit=1) + is_admin = record.has_group('base.group_system') + if is_admin: + record.company_ids1 = employee_id.company_id + else: + record.company_ids1 = record.company_ids + + def update_company_ids1(self): + cron_ids = self.env["res.users"].browse( + self._context.get('active_ids', self._context.get('active_id'))) + for cron in cron_ids: + cron._compute_company_ids1() + + @api.model + def create(self, vals): + user = super(ResUsers, self).create(vals) + user._compute_company_ids1() + return user + + + def all_create_users(self): + employee_ids = self.browse(self._context.get('active_ids', self._context.get('active_id'))) + for employee in employee_ids: + employee_id = self.env['hr.employee'].search([('name', '=', employee.name)]) + if not employee_id: + employee_data = { + 'name': employee.name, + 'company_id': employee.env.company.id, + 'user_id': employee.id, + } + self.env['hr.employee'].create(employee_data) + else: + raise UserError('该用户:%s已经创建员工,请不要重复操作' % employee_id.name) + + def all_users_email(self): + employee_ids = self.browse(self._context.get('active_ids', self._context.get('active_id'))) + for employee in employee_ids: + if not employee.partner_id.email: + pinyin_list = pypinyin.lazy_pinyin(employee.name, style=pypinyin.NORMAL) + name_pinyin = ''.join(pinyin_list).lower() + email = f"{name_pinyin}@thtjzt.com" + employee.partner_id.write({'email': email}) \ No newline at end of file diff --git a/yuthon_hr_employee/models/yuthon_hr_employee.py b/yuthon_hr_employee/models/yuthon_hr_employee.py new file mode 100644 index 0000000..386ed9d --- /dev/null +++ b/yuthon_hr_employee/models/yuthon_hr_employee.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- + +from dateutil.relativedelta import relativedelta +from pypinyin import pinyin, lazy_pinyin, STYLE_FIRST_LETTER +from odoo.osv import expression +from odoo import api, fields, models +from datetime import date, datetime +from odoo.exceptions import UserError, ValidationError + +nation_list = [('a', '汉族'), ('b', '壮族'), ('v', '满族'), ('d', '回族'), ('e', '苗族'), ('f', '维吾尔族'), + ('g', '土家族'), ('h', '彝族'), ('i', '蒙古族'), ('j', '藏族'), ('k', '布依族'), ('l', '侗族'), + ('m', '瑶族'), ('n', '朝鲜族'), ('o', '白族'), ('p', '哈尼族'), ('q', '哈萨克族'), ('r', '黎族'), + ('s', '傣族'), ('t', '畲族'), ('u', '傈僳族'), ('v', '仡佬族'), ('w', '东乡族'), ('x', '拉祜族'), + ('y', '水族'), ('z', '佤族'), ('a1', '纳西族'), ('b1', '羌族'), ('c1', '仫佬族'), ('d1', '锡伯族'), + ('e1', '柯尔克孜族'), ('f1', '达斡尔族'), ('g1', '景颇族'), ('h1', '毛南族'), ('i1', '撒拉族'), + ('j1', '布朗族'), ('k1', '塔吉克族'), ('l1', '阿昌族'), ('m1', '普米族'), ('n1', '鄂温克族'), ('o1', '怒族'), + ('p1', '京族'), ('q1', '基诺族'), ('r1', '德昂族'), ('s1', '保安族'), ('t1', '俄罗斯族'), ('u1', '裕固族'), + ('v1', '乌孜别克族'), ('w1', '门巴族'), ('x1', '鄂伦春族'), ('y1', '独龙族'), ('z1', '塔塔尔族'), ('a2', '赫哲族'), + ('b2', '高山族'), ('c2', '珞巴族'), ('d2', '壮族')] + + +class YuthonHrEmployee(models.Model): + """ + 人员管理系统 - 教学版 + 对 hr.employee 模型的扩展,添加个人信息、工作信息、教育经历等字段 + """ + _inherit = "hr.employee" + _order = 'number asc' + + # 基本信息 + compress_image = fields.Binary(string="压缩头像", attachment=True) + company_ids = fields.Many2many('res.company', related="user_id.company_ids") + number = fields.Char(string="序号", store=True) + is_soldiers = fields.Boolean(string="是否为退伍军人") + user_number = fields.Char(string="员工编号", default=lambda self: self.env['ir.sequence'].next_by_code('employee_code')) + use_user_type = fields.Selection([('formal_one', '正式合同(一签)'), ('labor', '劳务协议'), ('formal_two', '正式合同(二签)'), + ('formal_none', '正式合同(无固定)'), + ('part_time', '兼职协议'), ('employment', '用工协议'), ('internship', '实习协议')], string="用工形式") + date_end = fields.Date(string="合同结束日期") + end_comparison = fields.Selection([('red', '小于30天'), ('yellow', '小于60天'), ('blue', '小于90天')], string="结束时间对比(颜色)", + compute='_compute_end_comparison', store=True) + + # 个人信息 + user_sex = fields.Selection([('girl', '女'), ('boy', '男')], string="性别") + user_state = fields.Selection([('in', '在职'), ('leave', '离职'), ('withdraw', '退休'), ('borrow', '借调'), + ('expatriate', '外派'), ('adjunct', '在职(兼职)')], + string="员工状态", default='in') + user_number_start_time = fields.Date(string="身份证开始时间") + department_ids = fields.Many2many('hr.department', string="辅助部门") + user_number_end_time = fields.Date(string="身份证到期时间") + is_identification = fields.Boolean(string="是否填写身份证") + card_documents_id = fields.Many2one('ir.attachment', string="身份证正面") + card_back_documents_id = fields.Many2one('ir.attachment', string="身份证反面") + hukou_documents_id = fields.Many2one('ir.attachment', string="户口本") + marriage_documents_id = fields.Many2one('ir.attachment', string="结婚证") + nation = fields.Selection(nation_list, string="民族") + old = fields.Integer(string="年龄", compute='_compute_info', store=True) + birthday = fields.Date(string="出生年月", compute='_compute_info', tracking=False) + birthday_month = fields.Integer(string="生日月份", compute='_compute_info') + user_type = fields.Char(string="人员类别") + retire_date = fields.Date(string="退休日期") + retire_years = fields.Integer(string="离退休年限", compute='_compute_retire_years') + political_aspects = fields.Selection([('cpc_dang', '中共党员'), ('nld', '民盟'), + ('league_member', '共青团员'), ('people', '群众')], string="政治面貌") + dang_data = fields.Date(string="入团(党)时间") + user_origin = fields.Char(string="籍贯") + user_location = fields.Char(string="户口所在地") + is_location = fields.Boolean(string="是否本地") + bank_account = fields.Char(string="银行卡号") + user_location_type = fields.Selection([('formal', '本地农业'), ('labor', '本地非农业'), + ('part_time', '外地农业'), ('practice', '外地非农业')], string="户籍类型") + is_local_location = fields.Char( + string="是否外地", + compute='_compute_is_local_location', + ) + + @api.depends('user_location_type') + def _compute_is_local_location(self): + for rec in self: + if rec.user_location_type in ('formal', 'labor'): + rec.is_local_location = '否' + else: + rec.is_local_location = '是' + + # 工作信息 + entry_date = fields.Date(string="入职日期") + confirmation_date = fields.Date(string="转正日期") + company_old = fields.Char(string="司龄", compute='_compute_company_old') + company_old_year = fields.Integer(string="司龄(年)", compute='_compute_company_old') + regularization_year = fields.Integer(string="转正(年)", compute='_compute_regularization_year') + open_start_time = fields.Date(string="参加工作时间") + is_membership = fields.Boolean(string="工会会员") + cumulative_work_month = fields.Integer(string="累计工作月数", compute='_compute_social_insurance_info') + cumulative_work_years = fields.Char(string="累计工作年限", compute='_compute_cumulative_work_years') + interrupt_work_month = fields.Integer(string="中断工作月数") + compute_char = fields.Char(string="工龄", compute='_compute_social_insurance_info') + compute_month = fields.Integer(string="工龄(月)") + social_insurance_date = fields.Date(string="缴纳社保日期") + social_insurance_time = fields.Char(string="社保时长", compute='_compute_social_insurance_info', store=True) + + # 教育信息 + high_degree = fields.Char(string="最高学历") + high_degree2 = fields.Char(string="最高学位") + graduate_school = fields.Char(string="毕业院校") + profession = fields.Char(string="专业") + graduate_date = fields.Date(string="毕业时间") + job_lv_date = fields.Char(string="职级等级/签发日期(人事员)") + job_lv = fields.Char(string="专业技术/技能等级(劳动局)") + job_qualification_date = fields.Date(string="职业资格/考取日期") + job_lv_change = fields.Char(string="职级变动") + move_note_date = fields.Text(string="异动情况/移动日期") + + # 紧急联系人 + emergency_contacts_name = fields.Char(string="称谓") + + # 其他信息 + archiving_agency = fields.Char(string="存档机构") + over_data_time = fields.Datetime(string="最后更新数据时间") + + # 紧急联系人 + emergency_contact_ids = fields.One2many('yuthon.emergency.contact.line', 'employee_id', string="紧急联系人明细行") + # 工作经历 + work_experience_ids = fields.One2many('yuthon.work.experience.line', 'employee_id', string="工作经历明细行") + # 教育经历 + education_experience_ids = fields.One2many('yuthon.education.experience.line', 'employee_id', string="教育经历明细行") + # 技能证书管理 + skills_certificates_ids = fields.One2many('yuthon.skills.certificates.line', 'employee_id', string="教育经历明细行") + + # 拼音搜索 + pinyin_name = fields.Char(string="拼音名称", compute='_compute_pinyin', index=True, store=True) + pinyin_min = fields.Char(string='拼音缩写', compute='_compute_pinyin', index=True, store=True) + + is_company = fields.Boolean(string="本公司", default=True, compute='_compute_is_company', search='_search_part_of_company') + resource_calendar_id = fields.Many2one(compute='_compute_resource_calendar_id', string="工作时间", store=True) + + marital = fields.Selection([ + ('single', '未婚'), + ('married', 'Married'), + ('cohabitant', 'Legal Cohabitant'), + ('widower', 'Widower'), + ('divorced', 'Divorced') + ], string='Marital Status', groups="hr.group_hr_user", default='single', tracking=True) + + def get_contract_count(self): + return self.env['hr.employee'].search_count([('end_comparison', 'in', ['red', 'yellow', 'blue'])]) + + def update_end_comparison(self): + employees = self.env['hr.employee'].search([]) + for employee in employees: + employee._compute_end_comparison() + + @api.depends('date_end') + def _compute_end_comparison(self): + for record in self: + now = date.today() + if record.date_end: + if record.date_end > now: + delta = record.date_end - now + if 60 <= delta.days < 90: + record.end_comparison = 'blue' + elif 30 <= delta.days < 60: + record.end_comparison = 'yellow' + elif delta.days < 30: + record.end_comparison = 'red' + else: + record.end_comparison = None + elif record.date_end < now: + record.end_comparison = 'red' + else: + record.end_comparison = None + else: + record.end_comparison = None + + def get_my_employee_action(self): + current_user = self.env.user + my_employee = self.search([('user_id', '=', current_user.id)], limit=1) + return { + 'type': 'ir.actions.act_window', + 'name': '个人基础信息', + 'res_model': 'hr.employee', + 'res_id': my_employee.id, + 'view_mode': 'form', + 'view_type': 'form', + 'target': 'current', + 'view_id': self.env.ref('yuthon_hr_employee.view_basic_personal_information').id, + } + + def change_password_wizard(self): + return { + 'name': '修改密码', + 'type': 'ir.actions.act_window', + 'res_model': 'reset.password.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_employee_id': self.env.user.employee_ids.id}, + } + + @api.depends('confirmation_date') + def _compute_regularization_year(self): + for record in self: + if record.confirmation_date: + record.regularization_year = fields.Date.today().year - record.confirmation_date.year + else: + record.regularization_year = 0 + + @api.depends('entry_date') + def _compute_company_old(self): + """司龄计算""" + for record in self: + if record.entry_date: + today = datetime.now().date() + record.company_old_year = today.year - record.entry_date.year + 1 + years_diff = today.year - record.entry_date.year + months_diff = today.month - record.entry_date.month + if today.month < record.entry_date.month: + years_diff -= 1 + months_diff += 12 + record.company_old = str(years_diff) + '年' + str(months_diff) + '月' + else: + record.company_old = None + record.company_old_year = 0 + + @api.depends('open_start_time', 'interrupt_work_month') + def _compute_social_insurance_info(self): + for record in self: + cumulative_months = 0 + if record.open_start_time: + today = datetime.now().date() + delta = relativedelta(today, record.open_start_time) + cumulative_months = delta.years * 12 + delta.months + record.cumulative_work_month = cumulative_months + total_months = cumulative_months - record.interrupt_work_month + record.compute_month = total_months + if total_months <= 0: + social_insurance_time_str = "0个月" + else: + years = total_months // 12 + months = total_months % 12 + social_insurance_time_str = f"{years}年{months}个月" if years > 0 and months > 0 else ( + f"{years}年" if years > 0 else f"{months}个月") + record.social_insurance_time = social_insurance_time_str + record.compute_char = social_insurance_time_str + + @api.depends('cumulative_work_month') + def _compute_cumulative_work_years(self): + for record in self: + if record.cumulative_work_month: + if record.cumulative_work_month <= 0: + record.social_insurance_time = "0个月" + years = record.cumulative_work_month // 12 + months = record.cumulative_work_month % 12 + if years > 0: + result_str = f"{years}年" + if months > 0: + result_str += f"{months}个月" + else: + result_str = f"{months}个月" + record.cumulative_work_years = result_str + else: + record.cumulative_work_years = None + + @api.depends('identification_id') + def _compute_info(self): + for record in self: + if record.identification_id: + self.is_identification = True + id_number = record.identification_id.upper() + if len(id_number) != 18: + record._clear_info() + if not (all(c.isdigit() for c in id_number[:-1]) and ( + id_number[-1].isdigit() or id_number[-1] == 'X')): + raise ValueError("身份证号码包含无效字符") + + year = int(id_number[6:10]) + month = int(id_number[10:12]) + day = int(id_number[12:14]) + record.birthday = date(year, month, day) + record.birthday_month = month + + today = date.today() + if year is not None: + record.old = today.year - year - ((today.month, today.day) < (month, day)) + else: + record.old = 0 + gender_code = int(id_number[-2]) + record.user_sex = 'girl' if gender_code % 2 == 0 else 'boy' + else: + record._clear_info() + self.is_identification = False + + def _clear_info(self): + self.birthday = False + self.birthday_month = False + self.old = 0 + self.user_sex = False + + @api.depends('retire_date') + def _compute_retire_years(self): + """离退休年限计算""" + for record in self: + if record.retire_date: + today = datetime.now().date() + years_diff = today.year - record.retire_date.year + compute = years_diff + if compute < 0: + record.retire_years = -1 * compute + else: + record.retire_years = 0 + else: + record.retire_years = 0 + + @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): + if operator not in ('=', '!=') or not isinstance(value, bool): + raise UserError(('Operation not supported')) + 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)] + + @api.onchange('user_location') + def _onchange_user_location(self): + if self.user_location: + self.is_location = True + else: + self.is_location = False + + @api.depends('name') + def _compute_pinyin(self): + for employee in self: + employee.pinyin_name = ''.join(lazy_pinyin(employee.name)) if employee.name else '' + employee.pinyin_min = ''.join(lazy_pinyin(employee.name, style=STYLE_FIRST_LETTER)) if employee.name else '' + + @api.model + def _name_search(self, name, domain=None, operator='ilike', limit=None, order=None): + domain = domain or [] + if name: + has_chinese = any('\u4e00' <= char <= '\u9fff' for char in name) + if has_chinese: + search_domain = [('name', operator, name)] + else: + search_domain = ['|', + ('pinyin_name', operator, name), + ('name', '=', name)] + domain = expression.AND([search_domain, domain]) + return self._search(domain, limit=limit, order=order) + + def action_hr_employee(self): + return { + "type": "ir.actions.act_window", + "name": "批量更新员工", + "res_model": "yuthon.hr.employee.wizard", + "view_mode": "form", + "target": "new", + } + + def action_hr_time_form(self): + return { + "type": "ir.actions.act_window", + "name": "批量更新时间表", + "res_model": "yuthon.hr.employee.time.wizard", + "view_mode": "form", + "target": "new", + } diff --git a/yuthon_hr_employee/models/yuthon_hr_employee_pages.py b/yuthon_hr_employee/models/yuthon_hr_employee_pages.py new file mode 100644 index 0000000..ab989c5 --- /dev/null +++ b/yuthon_hr_employee/models/yuthon_hr_employee_pages.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models + + +class YuthonWorkExperienceLine(models.Model): + _name = 'yuthon.work.experience.line' + _description = "工作经验明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + start_date = fields.Date(string="开始日期") + end_date = fields.Date(string="结束日期") + work_place = fields.Char(string="工作单位") + post = fields.Char(string="岗位") + contact_user = fields.Char(string="联系人") + phone_number = fields.Char(string="联系人手机号") + reason_leaving = fields.Char(string="离职原因") + + +class YuthonEducationExperienceLine(models.Model): + _name = 'yuthon.education.experience.line' + _description = "教育经历明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + name_school = fields.Char(string="学校名称") + start_date = fields.Date(string="入学时间") + end_date = fields.Date(string="毕业时间") + in_school_duties = fields.Char(string="在校职务") + professional = fields.Char(string="专业") + learning_style = fields.Selection([('full_time', '全日制'), ('upgrade', '专升本'), ('adult_education', '成人教育'), + ('self_study', '自考'), ('online_education', '网络教育'),('correspondence', '函授')], string="学习方式") + part_time_degree = fields.Char(string="学历") + full_time_degree = fields.Char(string="学位") + documents_ids = fields.Many2many('ir.attachment', string="附件") + +class YuthonEmergencyContactLine(models.Model): + _name = 'yuthon.emergency.contact.line' + _description = "紧急联系人明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + name = fields.Char(string="姓名") + user_sfz = fields.Char(string="身份证") + user_relationship = fields.Selection([("father", "父亲"), ('mother', '母亲'), ("spouse", "妻子"), + ('boys', '儿子'), ('daughter', '女儿'),('didi', '弟弟'), + ('husband', '丈夫'), ('sister', '姐姐'), ('brother', '哥哥'), + ('sister_do', '妹妹')], string="关系") + workplace = fields.Char(string="工作单位") + post = fields.Char(string="岗位") + phone_number = fields.Char(string="手机号") + home_address = fields.Char(string="家庭住址") + documents_ids = fields.Many2many('ir.attachment', string="附件") + + +class YuthonSkillsCertificatesLine(models.Model): + _name = 'yuthon.skills.certificates.line' + _description = "技能证书管理明细行" + + employee_id = fields.Many2one('hr.employee', string="关联员工") + rank_name = fields.Char(string='职级名称') + rank_type = fields.Char(string='职级类型') + rank_time = fields.Date(string='职级时间') + documents_ids = fields.Many2many('ir.attachment', string="附件") diff --git a/yuthon_hr_employee/security/ir.model.access.csv b/yuthon_hr_employee/security/ir.model.access.csv new file mode 100644 index 0000000..0a7beab --- /dev/null +++ b/yuthon_hr_employee/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_yuthon_hr_employee_wizard,yuthon_hr_employee_wizard,model_yuthon_hr_employee_wizard,base.group_user,1,1,1,1 +access_yuthon_emergency_contact_line,yuthon_emergency_contact_line,model_yuthon_emergency_contact_line,base.group_user,1,1,1,1 +access_yuthon_work_experience_line,yuthon_work_experience_line,model_yuthon_work_experience_line,base.group_user,1,1,1,1 +access_yuthon_education_experience_line,yuthon_education_experience_line,model_yuthon_education_experience_line,base.group_user,1,1,1,1 +access_yuthon_skills_certificates_line,yuthon_skills_certificates_line,model_yuthon_skills_certificates_line,base.group_user,1,1,1,1 +access_yuthon_hr_employee_time_wizard,yuthon_hr_employee_time_wizard,model_yuthon_hr_employee_time_wizard,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 diff --git a/yuthon_hr_employee/static/description/icon.png b/yuthon_hr_employee/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7c14d2c982d996a1de1145d96bd465e440b58717 GIT binary patch literal 6522 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 C{#rHw literal 0 HcmV?d00001 diff --git a/yuthon_hr_employee/static/description/icon.svg b/yuthon_hr_employee/static/description/icon.svg new file mode 100644 index 0000000..95b3e24 --- /dev/null +++ b/yuthon_hr_employee/static/description/icon.svg @@ -0,0 +1 @@ + diff --git a/yuthon_hr_employee/static/others/user_template.xlsx b/yuthon_hr_employee/static/others/user_template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2c4f3b209814ae6ffda1f8ed13d9956ca28e2cdf GIT binary patch literal 12829 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 JwyB=+{yzkj+n4|V literal 0 HcmV?d00001 diff --git a/yuthon_hr_employee/static/scss/yuthon_employee_scss.scss b/yuthon_hr_employee/static/scss/yuthon_employee_scss.scss new file mode 100644 index 0000000..c35fa00 --- /dev/null +++ b/yuthon_hr_employee/static/scss/yuthon_employee_scss.scss @@ -0,0 +1,13 @@ +.list_width{ + width: 400px !important; + max-width: 400px !important; +} +.list_ids_width{ + width: 400px !important; + max-width: 400px !important; +} + +.list_employee_width{ + width: 80px !important; + max-width: 80px !important; +} \ No newline at end of file diff --git a/yuthon_hr_employee/views/inherit_hr_job_views.xml b/yuthon_hr_employee/views/inherit_hr_job_views.xml new file mode 100644 index 0000000..b0fcda3 --- /dev/null +++ b/yuthon_hr_employee/views/inherit_hr_job_views.xml @@ -0,0 +1,206 @@ + + + + hr.job.inherit + hr.job + + + + 20 + + + 岗位 + + + compiling_status == '1' + + + 所属部门 + + + 1 + + + 1 + + + 1 + + + + + + + + + + + + + + + + + + + hr.job.inherit.form + hr.job + + + + 基础信息 + + + 任职要求 + + + 其他信息 + + + 1 + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + 所属部门 + 1 + + + + 1 + + + + 1 + + + + 1 + + + 1 + + + 1 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + custom.hr.job.form.inherit + hr.job + + + + 1 + + + + + + hr.job.form1 + hr.job + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + +
+ diff --git a/yuthon_hr_employee/views/inherit_hr_work_location_views.xml b/yuthon_hr_employee/views/inherit_hr_work_location_views.xml new file mode 100644 index 0000000..3db4d27 --- /dev/null +++ b/yuthon_hr_employee/views/inherit_hr_work_location_views.xml @@ -0,0 +1,17 @@ + + + + hr.work.location.inherit.form + hr.work.location + + + + 0 + + + 联系人 + + + + + diff --git a/yuthon_hr_employee/views/inherit_res_groups_views.xml b/yuthon_hr_employee/views/inherit_res_groups_views.xml new file mode 100644 index 0000000..e6bc12d --- /dev/null +++ b/yuthon_hr_employee/views/inherit_res_groups_views.xml @@ -0,0 +1,28 @@ + + + + unlink.res.groups.form + res.groups + + + + 0 + + +
+
+
+ + + + + + + many2many_checkboxes + +
+
+
diff --git a/yuthon_hr_employee/views/inherit_res_user_views.xml b/yuthon_hr_employee/views/inherit_res_user_views.xml new file mode 100644 index 0000000..4e499e6 --- /dev/null +++ b/yuthon_hr_employee/views/inherit_res_user_views.xml @@ -0,0 +1,29 @@ + + + + 批量创建员工 + + + code + records.all_create_users() + + + + 更新邮件信息 + + + code + records.all_users_email() + + + + res.users.tree.inherit + res.users + + + + 1 + + + + \ No newline at end of file diff --git a/yuthon_hr_employee/views/yuthon_hr_employee_views.xml b/yuthon_hr_employee/views/yuthon_hr_employee_views.xml new file mode 100644 index 0000000..08c97cc --- /dev/null +++ b/yuthon_hr_employee/views/yuthon_hr_employee_views.xml @@ -0,0 +1,524 @@ + + + + hr.employee.basic.personal.information + hr.employee + +
+ +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + hr.employee.inherit.del.form.inherit + hr.employee + + + + + 0 + + + 上级领导 + "{'no_create': True}" + + + 1 + + + 1 + "{'no_open': True}" + + + 1 + + + "{'no_open': True}" + + + "{'no_open': True}" + + + "{'no_open': True}" + + + + + + + ztree_select + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + "{'no_open': True}" + + + "{'no_create': True, 'no_open': True}" + + + + + + + 1 + + + 1 + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hr.employee.inherit.del.tree + hr.employee + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + hr.employee.inherit.del.form + hr.employee + + + + + + + + + + + + hr.employee.contacts.search + hr.employee + + + + + + + + hr.employee.search + hr.employee + + + + + + + + + + hr.employee.member + hr.employee + + + + + + + + + + + + + + + + + + + + hr.employee.member + hr.employee + + + + + + + + + + hr.employee.inherit.search + hr.employee + + + + + + + + + + + + + + + + + + + + + + + + 批量更新员工 + + + code + action = model.action_hr_employee() + + + + list,kanban,form,activity,graph,pivot + + + + 通讯录 + hr.employee + [('is_company', '=', True)] + list,kanban,form,search + + + + + 个人基础信息 + + + code + + action = model.get_my_employee_action() + + + + + 重置密码 + + + code + + action = model.change_password_wizard() + + + + 会员列表 + hr.employee + [('is_membership', '=', True), ('is_company', '=', True)] + list,kanban,form,search + + + + + list,kanban,form,activity,graph,pivot + + + + + + + + + + + + + + + + + + + + + +
diff --git a/yuthon_hr_employee/views/yuthon_inherit_hr_employee.xml b/yuthon_hr_employee/views/yuthon_inherit_hr_employee.xml new file mode 100644 index 0000000..0bd210f --- /dev/null +++ b/yuthon_hr_employee/views/yuthon_inherit_hr_employee.xml @@ -0,0 +1,131 @@ + + + + yuthon.inherit.hr.employee + hr.employee + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yuthon_hr_employee/wizard/__init__.py b/yuthon_hr_employee/wizard/__init__.py new file mode 100644 index 0000000..6cca9d4 --- /dev/null +++ b/yuthon_hr_employee/wizard/__init__.py @@ -0,0 +1,3 @@ +from . import hr_employee_wizard +from . import hr_employee_time_wizard +from . import reset_password_wizard diff --git a/yuthon_hr_employee/wizard/hr_employee_time_wizard.py b/yuthon_hr_employee/wizard/hr_employee_time_wizard.py new file mode 100644 index 0000000..acde4d9 --- /dev/null +++ b/yuthon_hr_employee/wizard/hr_employee_time_wizard.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _, SUPERUSER_ID +from odoo.exceptions import ValidationError, UserError + + +class YuthonHrEmployeeTimeWizard(models.Model): + _name = 'yuthon.hr.employee.time.wizard' + + department_id = fields.Many2one('hr.department', string='部门') + employee_ids = fields.Many2many('hr.employee', compute='_compute_all_employees', string='所有员工' ,store=True, ) + attendance_id = fields.Many2one('resource.calendar', string='考勤时间表') + + @api.depends('department_id') + def _compute_all_employees(self): + for record in self: + if record.department_id: + record.employee_ids = self.get_all_employees(record.department_id) + else: + record.employee_ids = False + + def get_all_employees(self, department): + employees = self.env['hr.employee'].search([('department_id', '=', department.id)]) + child_departments = self.env['hr.department'].search([('parent_id', '=', department.id)]) + for child_department in child_departments: + employees += self.get_all_employees(child_department) + + return employees + + def confirm(self): + for employee_id in self.employee_ids: + if self.attendance_id: + employee_id.write({'resource_calendar_id': self.attendance_id.id}) + else: + employee_id.write({'resource_calendar_id': False}) \ No newline at end of file diff --git a/yuthon_hr_employee/wizard/hr_employee_time_wizard_views.xml b/yuthon_hr_employee/wizard/hr_employee_time_wizard_views.xml new file mode 100644 index 0000000..93cbe6b --- /dev/null +++ b/yuthon_hr_employee/wizard/hr_employee_time_wizard_views.xml @@ -0,0 +1,22 @@ + + + + 考勤时间表 + yuthon.hr.employee.time.wizard + +
+ + + + + + + +
+
+
+
+
+
\ No newline at end of file diff --git a/yuthon_hr_employee/wizard/hr_employee_wizard.py b/yuthon_hr_employee/wizard/hr_employee_wizard.py new file mode 100644 index 0000000..f8b815d --- /dev/null +++ b/yuthon_hr_employee/wizard/hr_employee_wizard.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _, SUPERUSER_ID +from odoo.exceptions import ValidationError, UserError +from datetime import datetime, timedelta +import xlrd +import base64 +import datetime +from datetime import datetime +import pandas as pd + + +class YuthonHrEmployeeWizard(models.Model): + _name = 'yuthon.hr.employee.wizard' + + excel_file = fields.Binary(string="上传Excel表") + excel_name = fields.Char(string="文件名") + + def confirm(self): + """ + 导入excel文件,按行读取数据并处理 + :return: 导入员工信息表并更新 + """ + if not self.excel_file: + raise ValidationError('请上传文件。') + book = xlrd.open_workbook(file_contents=base64.decodebytes(self.excel_file)) + sh = book.sheet_by_index(0) + for rx in range(1, sh.nrows): + row = sh.row(rx) + self.update_bank_id(row) + return { + 'name': '批量更新员工', + 'type': 'ir.actions.act_window', + 'view_mode': 'list', + 'res_model': 'hr.employee', + } + + def employee_user_data(self, row): + employee_name = self.env['hr.employee'].search([('name', '=', row[0].value)]) + base_date = datetime(1900, 1, 1) + if row[1].value: + entry_date = base_date + timedelta(days=row[1].value - 2) if row[1].value > 0 else base_date + else: + entry_date = None + if row[2].value: + open_start_time = base_date + timedelta(days=row[2].value - 2) if row[2].value > 0 else base_date + else: + open_start_time = None + # if row[6].value: + # dang_data = base_date + timedelta(days=row[6].value - 2) if row[6].value > 0 else base_date + # else: + # dang_data = None + # political_aspects_dict = {'中共党员': 'cpc_dang', '民盟': 'nld', '共青团员': 'league_member', '群众': 'people', '': ''} + # political_aspects = political_aspects_dict[row[5].value] + # card = row[1].value if row[1].value else None + # last_attendance = row[7].value + # if row[2].value: + # date1 = pd.to_datetime(row[2].value, unit='d', origin='1899-12-30') + # else: + # date1 = None + # interrupt_work_month = row[3].value + # number = row[0].value + employee_date = { + 'entry_date': entry_date, + 'open_start_time': open_start_time, + # 'dang_data':dang_data, + # 'identification_id':card, + # 'political_aspects': political_aspects, + # 'last_attendance': last_attendance, + } + if employee_name: + employee_name.write(employee_date) + + + # text = row[1].value + # text = text.replace('\\', '') + # if department_ids: + # department_id = department_ids[0] + # else: + # raise UserError(f'{text}不存在,请检查。') + # = self.env['hr.job'].search([('name', '=', row[2].value)]) + # use_user_type_dict = {'正式职工': 'formal', '劳务协议': 'labor', '兼职协议': 'part_time', '兼职用工': 'employment', '': ''} + # use_user_type = use_user_type_dict[row[1].value] + # job_lv_id = self.env['yuthon.job.lv'].search([('name', '=', row[9].value)]) + # marital_dict = {'单身': 'single', '已婚': 'married', '合法同居者': 'cohabitant', '丧偶': 'widower', '离异': 'divorced', '': ''} + # marital = marital_dict[row[19].value] + # role = row[4].value.split(',') + # role_list =[] + # user_role_id = self.env['soong.user.role'] + # for i in role: + # role_id = user_role_id.search([('name', '=', i)]) + # if not role_id: + # role_id = user_role_id.create({'name': i}) + # role_list.append(role_id.id) + # use_user_type_dict = {'正式合同': 'formal_one', '劳务协议': 'labor', '正式合同(二签)': 'formal_two', '正式合同(无固定)': 'formal_none', + # '兼职协议': 'part_time', '用工协议': 'employment', '实习协议': 'internship'} + # + # user_role_id = self.env['soong.user.role'].search([('name', '=', row[3].value)], limit=1) + # job_id = self.env['hr.job'].search([('name', '=', row[4].value)]) + # parent_id = self.env['hr.employee'].search([('name', '=', row[6].value)]) + # open_start_time = row[17].value + # if open_start_time: + # if type(open_start_time) == float: + # entry_date = datetime.fromtimestamp(open_start_time) + # else: + # entry_date = datetime.strptime(open_start_time, '%Y-%m-%d') + # phone = row[21].value if row[21].value else None + # employee_date = { + # 'identification_id':card, + # 'open_start_time':entry_date, + # 'mobile_phone':phone, + # } + + # employee_data = { + # 'use_user_type': use_user_type if use_user_type else None, # 用工形式 + # 'marital': marital if marital else None, # 婚姻 + # 'user_type': row[23].value if row[23].value else None, # 人员类别 + # 'high_degree': row[30].value if row[30].value else None, # 最高学历 + # 'seniority_prize_pick_up_status': row[43].value if row[43].value else None, # 工龄相关年功奖领取情况 + # 'high_degree2': row[31].value if row[31].value else None, # 最高学位 + # 'new_start_contract_data': datetime.strptime(row[28].value, '%Y/%m/%d').date() if row[28].value else None, # 最新合同开始日期 + # 'political_aspects': political_aspects if political_aspects else None, # 政治面貌 + # 'birthday': datetime.strptime(row[20].value, '%Y/%m/%d').date() if row[20].value else None, # 出生年月 + # 'dang_data': datetime.strptime(row[27].value, '%Y/%m/%d').date() if row[27].value else None, # 入党(团)时间 + # 'retire_date': datetime.strptime(row[24].value, '%Y/%m/%d').date() if row[24].value else None, # 退休日期 + # 'new_end_contract_data': datetime.strptime(row[29].value, '%Y/%m/%d').date() if row[29].value else None, # 最新合同终止日期 + # 'job_lv_id': job_lv_id.id, # 最新职级 有几个没有 + # 'user_sex': 'boy' if row[18].value == '男' else 'girl', # 性别 + # 'nation': row[17].value if row[17].value else None, # 民族 + # 'identification_id': row[14].value if row[14].value else None, # 身份证号 + # 'entry_date': datetime.strptime(row[11].value, '%Y/%m/%d').date() if row[11].value else None, # 入职时间 + # 'confirmation_date': datetime.strptime(row[12].value, '%Y/%m/%d').date() if row[12].value else None, # 转正时间 + # 'user_number_start_time': datetime.strptime(row[15].value, '%Y/%m/%d').date() if row[15].value else None, # 身份证有效期开始时间 + # 'user_number_end_time': datetime.strptime(row[16].value, '%Y/%m/%d').date() if row[16].value else None, # 身份证有效期结束时间 + # 'mobile_phone': int(row[8].value) if row[8].value else None, # 手机号码 + # 'work_phone': int(row[7].value) if row[7].value else None, # 办公电话 + # 'user_number': row[3].value if row[3].value else None, # 员工编号 + # } + # if employee_name: + # employee_name.write(employee_date) + # + # # 处理 + # for rx in range(1, sh2.nrows): + # row2 = sh2.row(rx) + # if row2[0].value == row[4].value: + # existing_education = employee_name.education_experience_ids.filtered(lambda e: e.name_school == row2[1].value) + # education_data = { + # 'name_school': row2[1].value if row2[1].value else None, # 学校名称 + # 'end_date': datetime.strptime(row2[2].value, '%Y/%m/%d').date() if row2[2].value else None, # 毕业时间 + # 'professional': row2[3].value if row2[3].value else None, # 专业 + # 'Part_time_degree': row2[4].value if row2[4].value else None, # 非全日制学历学位 + # 'full_time_degree': row2[5].value if row2[5].value else None, # 全日制学历学位 + # } + # if existing_education: + # existing_education.write(education_data) + # else: + # employee_name.write({ + # 'education_experience_ids': [(0, 0, education_data)] + # }) + # + # for rx in range(1, sh3.nrows): + # row3 = sh3.row(rx) + # if row3[0].value == row[4].value: + # existing_certificate = employee_name.skills_certificates_ids.filtered(lambda s: s.rank_name == row3[1].value) + # certificate_data = { + # 'rank_name': row3[1].value if row3[1].value else None, # 职级名称 + # 'rank_time': datetime.strptime(row3[2].value, '%Y/%m/%d').date() if row3[2].value else None, # 职级时间 + # } + # if existing_certificate: + # existing_certificate.write(certificate_data) + # else: + # employee_name.write({ + # 'skills_certificates_ids': [(0, 0, certificate_data)] + # }) + def employee_date(self, value): + date = pd.to_datetime(value, unit='d', origin='1899-12-30').date() + return date + + + def update_bank_account(self,row): + contacts_list = [] + employee_id = self.env['hr.employee'].search([('name', '=', row[1].value)]) + company_id = self.env['res.company'].search([('name', '=', row[0].value)]) + if row[4].value: + work_phone = int(row[4].value) + else: + work_phone = None + if row[5].value: + mobile_phone = int(row[5].value) + else: + mobile_phone = None + if row[6].value: + entry_date = self.employee_date(row[6].value) + else: + entry_date = None + if row[7].value: + confirmation_date = self.employee_date(row[7].value) + else: + confirmation_date = None + identification_id = row[9].value + if row[10].value: + user_number_start_time = self.employee_date(row[10].value) + else: + user_number_start_time = None + if row[11].value and row[11].value != "长期": + user_number_end_time = self.employee_date(row[11].value) + else: + user_number_end_time = None + user_type = row[17].value + if row[18].value: + retire_date = self.employee_date(row[18].value) + else: + retire_date = None + + political_aspects_dict = {'中共党员': 'cpc_dang', '民盟': 'nld', '共青团员': 'league_member', '群众': 'people', + '': ''} + political_aspects = political_aspects_dict[row[20].value] + if row[21].value and row[21].value != "/": + dang_data = self.employee_date(row[21].value) + else: + dang_data = None + user_origin = row[22].value + user_location = row[23].value + if row[26].value: + open_start_time = self.employee_date(row[26].value) + else: + open_start_time = None + interrupt_work_month = row[27].value + cumulative_work_month = row[28].value + part_time_degree = row[29].value + full_time_degree = row[30].value + name_school = row[31].value + professional = row[32].value + job_lv = row[33].value + + private_street = row[35].value + contract_id = self.env['hr.contract'].search([('employee_id', '=', employee_id.id)]) + education_experience_list = [] + education_experience_list.append([0, 0, {'part_time_degree': part_time_degree, + 'full_time_degree': full_time_degree, + 'name_school': name_school, + 'professional': professional, + }]) + if row[24].value: + date_start = self.employee_date(row[24].value) + else: + date_start = None + date_end = row[25].value + if date_end == '无固定期限': + date_end = False + else: + date_end = self.employee_date(date_end) + contract_type_id = self.env['hr.contract.type'].search([('name', '=', row[34].value)]) + + if contract_id: + contract_id.write({ + 'date_start': date_start, + 'date_end': date_end, + 'contract_type_id': contract_type_id.id, + }) + + name = row[36].value + relationship_dict = { + '父亲': 'father', + '姐妹': 'sisters', + '妻子': 'spouse', + '女儿': 'daughter', + '丈夫': 'husband', + '姐姐': 'sister', + '兄弟': 'brother', + '妹妹': 'sister_do', + '父母': 'parents', + '子女': 'children', + '姐弟': 'siblings', + '母亲': 'mother', + '母女': 'daughter', + } + if row[37].value: + user_relationship = relationship_dict[row[37].value] + else: + user_relationship = False + phone_number = row[38].value + archiving_agency = row[39].value + contacts_list.append([0, 0, {'name': name, + 'user_relationship': user_relationship, + 'phone_number': phone_number, + }]) + write_uid = self.env['res.users'].search([('id', '=', employee_id.user_id.id)]) + user_type_dict = { + '正式合同(一签)': 'formal_one', + '劳务协议': 'labor', + '正式合同(二签)': 'formal_two', + '正式合同(无固定)': 'formal_none', + '兼职协议': 'part_time', + '用工协议': 'employment', + '实习协议': 'internship', + } + use_user_type = user_type_dict[row[34].value] + + employee_date= { + 'use_user_type': use_user_type, + 'company_id': company_id.id, + 'work_phone': work_phone, + 'mobile_phone': mobile_phone, + 'entry_date': entry_date, + 'dang_data':dang_data, + 'confirmation_date': confirmation_date, + 'user_number_start_time': user_number_start_time, + 'user_number_end_time': user_number_end_time, + 'retire_date': retire_date, + 'open_start_time': open_start_time, + 'identification_id': identification_id, + 'user_type': user_type, + 'political_aspects': political_aspects, + 'user_origin': user_origin, + 'user_location': user_location, + 'interrupt_work_month': interrupt_work_month, + 'private_street': private_street, + 'job_lv': job_lv, + 'write_uid': write_uid, + 'archiving_agency': archiving_agency, + 'emergency_contact_ids': contacts_list, + 'education_experience_ids': education_experience_list + } + # + if employee_id: + employee_id.write({'education_experience_ids': [(5, 0, 0)]}) + employee_id.write({'emergency_contact_ids': [(5, 0, 0)]}) + employee_id.write(employee_date) + + + def update_lc_employee(self, row): + employee_id = self.env['hr.employee'].search([('name', '=', row[0].value)]) + structure_type_id = self.env['hr.payroll.structure.type'].search([('name', '=', row[2].value), ('company_id.name', '=', '天河粮储')]) + if row[3].value: + entry_date = self.employee_date(row[3].value) + else: + entry_date = None + if row[4].value: + open_start_time = self.employee_date(row[4].value) + else: + open_start_time = None + interrupt_work_month = row[6].value + if employee_id: + employee_date = { + 'entry_date': entry_date, + 'open_start_time': open_start_time, + 'interrupt_work_month': interrupt_work_month, + } + employee_id.write(employee_date) + employee_id.contract_id.structure_type_id = structure_type_id.id, + + def update_bank_id(self, row): + employee_id = self.env['hr.employee'].search([('name', '=', row[0].value)]) + bank_account_id = self.env['res.partner.bank'].search([('acc_number', '=', row[1].value)], limit=1) + res_id = self.env['res.partner'].search([('name', '=', row[0].value)]) + if not bank_account_id: + bank_account_id = self.env['res.partner.bank'].create({ + 'acc_number': row[1].value, + 'partner_id': res_id.id, + }) + if employee_id: + employee_date = { + 'bank_account_id': bank_account_id.id, + } + employee_id.write(employee_date) + + + + diff --git a/yuthon_hr_employee/wizard/hr_employee_wizard_views.xml b/yuthon_hr_employee/wizard/hr_employee_wizard_views.xml new file mode 100644 index 0000000..0f48f3c --- /dev/null +++ b/yuthon_hr_employee/wizard/hr_employee_wizard_views.xml @@ -0,0 +1,34 @@ + + + + 员工模板 下载 + self + /yuthon_hr_employee/static/others/user_template.xlsx + + + 审批意见弹窗 + yuthon.hr.employee.wizard + +
+ +
+
+ + + + + + + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/yuthon_hr_employee/wizard/reset_password_wizard.py b/yuthon_hr_employee/wizard/reset_password_wizard.py new file mode 100644 index 0000000..cf99c63 --- /dev/null +++ b/yuthon_hr_employee/wizard/reset_password_wizard.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _, SUPERUSER_ID +from odoo.exceptions import ValidationError, UserError + + +class ResetPasswordWizard(models.TransientModel): + _name = 'reset.password.wizard' + + employee_id = fields.Many2one('hr.employee', string="关联员工") + password = fields.Char(string='新密码', required=True) + + def reset_password(self): + user = self.employee_id.user_id + user.sudo().password = self.password + diff --git a/yuthon_hr_employee/wizard/reset_password_wizard_viwes.xml b/yuthon_hr_employee/wizard/reset_password_wizard_viwes.xml new file mode 100644 index 0000000..53c0919 --- /dev/null +++ b/yuthon_hr_employee/wizard/reset_password_wizard_viwes.xml @@ -0,0 +1,21 @@ + + + + reset.password.wizard.form + reset.password.wizard + +
+ + + + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/yuthon_notice/__init__.py b/yuthon_notice/__init__.py index 724cd13..f5ba686 100644 --- a/yuthon_notice/__init__.py +++ b/yuthon_notice/__init__.py @@ -1,3 +1,2 @@ # -*- coding: utf-8 -*- -from . import models -from . import controllers \ No newline at end of file +from . import models \ No newline at end of file diff --git a/yuthon_notice/__manifest__.py b/yuthon_notice/__manifest__.py index fd3f9e7..e1432bb 100644 --- a/yuthon_notice/__manifest__.py +++ b/yuthon_notice/__manifest__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- { - 'name': "Yuthon notice", + 'name': "yuthon notice", 'summary': "通知公告", 'description': """ @@ -10,15 +10,15 @@ 'category': 'Human Resources/Attendances', 'version': '0.1', 'license': 'LGPL-3', - 'depends': ['base', 'hr', 'mail'], + 'depends': ['base', 'hr'], 'data': [ 'security/ir.model.access.csv', 'data/notice_code_data.xml', - 'data/notice_cron_data.xml', 'data/notice_type_data.xml', + 'report/notice_print_report.xml', + 'report/notice_print_template.xml', 'views/yuthon_notice_views.xml', 'views/yuthon_notice_type_views.xml', - 'views/yuthon_cs_tree_views.xml', 'views/yuthon_confirm_users_views.xml', ], 'assets': { @@ -30,4 +30,5 @@ }, 'installable': True, 'application': True, -} \ No newline at end of file +} + diff --git a/yuthon_notice/models/__init__.py b/yuthon_notice/models/__init__.py index b3a43c5..9bc41f1 100644 --- a/yuthon_notice/models/__init__.py +++ b/yuthon_notice/models/__init__.py @@ -3,4 +3,5 @@ from . import yuthon_notice from . import yuthon_notice_type from . import yuthon_confirm_users +from . import hr_department diff --git a/yuthon_notice/models/hr_department.py b/yuthon_notice/models/hr_department.py new file mode 100644 index 0000000..021c944 --- /dev/null +++ b/yuthon_notice/models/hr_department.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from odoo import api, models + + +class HrDepartment(models.Model): + _inherit = 'hr.department' + + @api.depends('name', 'parent_id.name') + @api.depends_context('show_parent_department') + def _compute_display_name(self): + """当上下文中 show_parent_department=True 时,显示 '直接上级 / 当前部门',只取一级""" + if self.env.context.get('show_parent_department'): + for record in self: + if record.parent_id: + record.display_name = '%s / %s' % (record.parent_id.name, record.name) + else: + record.display_name = record.name + return + return super()._compute_display_name() diff --git a/yuthon_notice/models/yuthon_confirm_users.py b/yuthon_notice/models/yuthon_confirm_users.py index a1e013c..747485e 100644 --- a/yuthon_notice/models/yuthon_confirm_users.py +++ b/yuthon_notice/models/yuthon_confirm_users.py @@ -6,33 +6,85 @@ from odoo.exceptions import UserError, ValidationError class YuthonConfirmUsers(models.Model): _name = "yuthon.confirm.users" _description = "公告确认信息" + _order = "employee_number asc" employee_id = fields.Many2one('hr.employee', string='员工') notice_id = fields.Many2one('yuthon.notice', string="公告名称") notice_code = fields.Char(string="公告编号") notice_name = fields.Char(string="公告名称") - state = fields.Selection([('no', '未确认'), ('yes', '已确认')], string="状态") + state = fields.Selection([('to_sent', '待发送'), ('no', '待确认'), ('yes', '已确认')], string="状态") + create_time = fields.Datetime(string='创建时间', default=fields.Datetime.now) + release_date = fields.Date(string="发布日期") + msg_id = fields.Char(string="Msg_id") + + # 从员工带出的字段 + company_id = fields.Many2one(related='employee_id.company_id', string='公司', store=True) + department_id = fields.Many2one(related='employee_id.department_id', string='部门', store=True) + employee_number = fields.Char(related='employee_id.number', string='序号', store=True) + + def withdrawn(self): + if self.state == 'to_sent': + raise UserError('该通知还未发送,无法撤回') + if self.msg_id: + self.sudo().env["wecom.apps"].back_message(self.msg_id, category='notice') + self.state = 'to_sent' + self.msg_id = '已撤回' + + def all_withdrawn(self): + confirm_noice_ids = self.env["yuthon.confirm.users"].browse(self._context.get('active_ids', self._context.get('active_id'))) + for record in confirm_noice_ids: + if record.state == 'state': + raise UserError(f'{record.notice_id.name}通知还未发送,无法撤回') + record.withdrawn() def sync_confirm_users_notices(self): - menu_id = self.env['ir.ui.menu'].search([('name', '=', '通知公告')]) for record in self.search([('state', '=', 'no')]): partner_ids = record.employee_id.user_id.partner_id.id record.with_context(lang=record.env.lang)._message_auto_subscribe_notify(partner_ids, 'yuthon_notice.message_yuthon_confirm_users_data') - url = f'https://oa.thtzjt.com/web#id={self.id}&cids=2&action={menu_id.action.id}&model=yuthon.notice&view_type=list&menu_id={menu_id.id}' - self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', user_id=record.employee_id.wecom_userid, title="通知公告", - description="您有待确认的通知公告请尽快确认", url=url, btntxt="详细信息") + url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id={record.employee_id.id}' + self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', touser=record.employee_id.wecom_userid, title="通知公告", + description=f"您有一条【{record.notice_id.name}】的通知公告请尽快确认", url=url, btntxt="详细信息") + def reminders_notice(self): - for rem in self: - if rem.state == 'yes': - raise UserError('已确认不可催办') confirm_noice_ids = self.env["yuthon.confirm.users"].browse(self._context.get('active_ids', self._context.get('active_id'))) + i = 0 for rec in confirm_noice_ids: - menu_id = self.env['ir.ui.menu'].search([('name', '=', '通知公告')]) - url = f'https://oa.thtzjt.com/web#id={self.id}&cids=2&action={menu_id.action.id}&model=yuthon.notice&view_type=list&menu_id={menu_id.id}' - self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', user_id=rec.employee_id.wecom_userid, - title="通知公告(催办)", description="您有待确认的通知公告请尽快确认", + url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id={rec.employee_id.id}' + result_data = self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', touser=rec.employee_id.wecom_userid, + title="通知公告(催办)", description=f"您有一条【{rec.notice_id.name}】的通知公告请尽快确认", url=url, btntxt="详细信息") - partner_ids = rec.employee_id.user_id.partner_id.id - rec.with_context(lang=rec.env.lang)._message_auto_subscribe_notify(partner_ids, - 'yuthon_notice.message_yuthon_confirm_users_data') + if result_data: + res_dict = result_data[1] + rec.write({'msg_id': res_dict.get('msgid')}) + if res_dict.get('errmsg') == 'ok': + i = i + 1 + url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id=298' + self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', + touser='18562027762', + title="通知公告", + description="成功发送消息通知" + str(i), + url=url, btntxt="详细信息") + + def reminders_notice_send(self): + confirm_noice_ids = self.env["yuthon.confirm.users"].browse( + self._context.get('active_ids', self._context.get('active_id'))) + i = 0 + for rec in confirm_noice_ids: + url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id={rec.employee_id.id}' + result_data = self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', + touser=rec.employee_id.wecom_userid, + title="通知公告", + description=f"您有一条【{rec.notice_id.name}】待确认的通知公告请尽快确认", + url=url, btntxt="详细信息") + if result_data: + res_dict = result_data[1] + rec.write({'msg_id': res_dict.get('msgid')}) + if res_dict.get('errmsg') == 'ok': + i = i + 1 + url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id=298' + self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', + touser='18562027762', + title="通知公告", + description="成功发送消息通知" + str(i), + url=url, btntxt="详细信息") \ No newline at end of file diff --git a/yuthon_notice/models/yuthon_notice.py b/yuthon_notice/models/yuthon_notice.py index 46cfa01..a7eaed0 100644 --- a/yuthon_notice/models/yuthon_notice.py +++ b/yuthon_notice/models/yuthon_notice.py @@ -2,6 +2,7 @@ from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError, UserError import logging +from datetime import datetime, timedelta, date _logger = logging.getLogger(__name__) @@ -22,17 +23,15 @@ class YuthonNotice(models.Model): create_date1 = fields.Date(string="发布日期", default=fields.Date.today()) start_date = fields.Date(string="生效日期", default=fields.Date.today()) end_date = fields.Date(string="终止日期") - sequence = fields.Integer(string="顺序",compute='_compute_sequence',store=True) + sequence = fields.Integer(string="顺序", default=1, compute='_compute_sequence') employee_ids = fields.Many2many('hr.employee', string="人员范围") department_ids = fields.Many2many('hr.department', string='部门范围') - users_role_ids = fields.Many2many('res.groups', string='角色范围') - users_role_group_ids = fields.Many2many('res.groups', string='角色组', relation='yuthon_notice_role_group_rel') notice_state = fields.Selection([('no', '未发布'), ('yes', '已发布'), ('stop', '已终止')], default='no', string="公告状态") - documents_ids = fields.Many2many('ir.attachment', string="附件") is_notice_roger = fields.Selection([('yes', '已读'), ('no', '未读')], default='no', string="确认状态") is_change_notice = fields.Boolean(string="是否发送通知", default=True) attn_notice = fields.Html(string="经办人意见") notice_text = fields.Html(string="正文") + notice_text1 = fields.Html(string="正文") read_number = fields.Integer(string='阅读量', compute="_compute_read_number", store=True) is_this_company = fields.Boolean(string='本公司', default=True) approval_user_id = fields.Many2one('res.users', string="审核人") @@ -41,6 +40,7 @@ class YuthonNotice(models.Model): urgency_level = fields.Selection([('normal', '普件'), ('urgent', '急件'), ('emergency', '特急件')], string='紧急度', default='normal') active = fields.Boolean('Active', default=True, tracking=True) + work_end = fields.Boolean(string="结束") users_ids = fields.Many2many( 'res.users', @@ -55,9 +55,36 @@ class YuthonNotice(models.Model): column1='notice_id', column2='user_id', string="当前经办人", - store=True, + default=lambda self: [(6, 0, [self.env.user.id])] ) + # @api.onchange('users_ids') + # def _onchange_users_ids(self): + # today = date.today() + # self.start_date = today + # self.create_date1 = today + + is_company = fields.Boolean(string="本公司", default=True, compute='_compute_is_company', + search='_search_part_of_company') + + publish_date = fields.Date(string='发布日期') + + @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)] + @api.model_create_multi def create(self, vals_list): for i in vals_list: @@ -72,42 +99,104 @@ class YuthonNotice(models.Model): @api.constrains('employee_ids', 'department_ids', 'users_role_ids', 'users_role_group_ids') def _constrains_employee_related_fields(self): - + """ + 约束验证:当关联字段变化时,更新users_ids + """ def get_department_employees(department_id): + """递归获取部门及其子部门的员工对应的user_id""" valid_user_ids = set() + # 查找当前部门的员工(使用id替代id.origin) employee_ids = self.env['hr.employee'].search([('department_id', '=', department_id.id)]) + # 收集有效的user_id valid_user_ids.update([emp.user_id.id for emp in employee_ids if emp.user_id]) + # 递归处理子部门 child_ids = self.env['hr.department'].search([('parent_id', '=', department_id.id)]) for child in child_ids: valid_user_ids.update(get_department_employees(child)) return valid_user_ids for notice in self: + # 避免重复触发约束(更新users_ids时跳过本次约束检查) if notice.env.context.get('skip_constrains'): continue - + # 获取部门关联的用户ID department_user_ids = set() for department_id in notice.department_ids: department_user_ids.update(get_department_employees(department_id)) - group_role_ids = notice.users_role_group_ids + group_role_ids = notice.users_role_group_ids.user_role_ids all_users = set( notice.employee_ids.mapped('user_id').ids + - group_role_ids.mapped('users').ids + - notice.users_role_ids.mapped('users').ids + + group_role_ids.mapped('personnel_ids.user_id').ids + + notice.users_role_ids.mapped('personnel_ids.user_id').ids + list(department_user_ids) ) notice.with_context(skip_constrains=True).users_ids = [(6, 0, list(all_users))] + if notice.users_ids and notice.is_change_notice: + conf_old = self.env['yuthon.confirm.users'].sudo().search([('notice_id', '=', notice.id)]) + old_employee_ids = conf_old.mapped('employee_id.id') + new_employee_ids = self.env['hr.employee'].sudo().search([ + ('user_id', 'in', notice.users_ids.ids) + ]).ids + add_employee_ids = list(set(new_employee_ids) - set(old_employee_ids)) + del_employee_ids = list(set(old_employee_ids) - set(new_employee_ids)) + + # 3. 处理删除: + if del_employee_ids: + del_conf = conf_old.filtered(lambda c: c.employee_id.id in del_employee_ids) + del_conf.unlink() + + if add_employee_ids: + for emp_id in add_employee_ids: + employee = self.env['hr.employee'].sudo().browse(emp_id) + if not employee: + continue + # 防重复:如果已经存在该员工的确认记录,跳过 + exist = self.env['yuthon.confirm.users'].sudo().search([ + ('notice_id', '=', notice.id), + ('employee_id', '=', emp_id), + ], limit=1) + if exist: + continue + self.env['yuthon.confirm.users'].sudo().create({ + 'notice_id': notice.id, + 'employee_id': emp_id, + 'notice_code': notice.code, + 'notice_name': notice.name, + 'release_date': notice.create_date1, + 'state': 'to_sent', + }) + def get_no_read_count(self): - return self.search_count([('is_notice_roger', '=', 'no'), ('notice_state', '=', 'yes')]) + # 只统计当前用户发布范围内的未读公告,与列表 domain 保持一致 + return self.search_count([ + ('users_ids', 'in', self.env.uid), + ('is_notice_roger', '=', 'no'), + ('notice_state', '=', 'yes'), + ]) def read(self, fields=None, load='_classic_read'): + # 标记是否真的有未读变已读,避免没变化也触发菜单刷新 + changed = False for con in self: - con_id = self.env['yuthon.confirm.users'].search([('notice_id', '=', con.id), ('employee_id', '=', con.env.user.employee_id.id)]) - if con_id.employee_id == con.env.user.employee_id: - con.is_notice_roger = 'yes' - con_id.write({'state': 'yes'}) - return super(YuthonNotice, self).read(fields=fields, load=load) + # 只有全局状态仍是未读时才处理 + if con.is_notice_roger == 'no': + # 查找当前用户对应的公告确认记录 + con_id = self.env['yuthon.confirm.users'].search([ + ('notice_id', '=', con.id), + ('employee_id', '=', con.env.user.employee_id.id) + ]) + # 确认是当前员工本人,再标记为已读 + if con_id.employee_id == con.env.user.employee_id: + con.is_notice_roger = 'yes' + con_id.write({'state': 'yes'}) + changed = True + res = super(YuthonNotice, self).read(fields=fields, load=load) + # 如果有记录从 未读 变为 已读,清除菜单缓存并通知前端刷新角标 + if changed: + self.env.registry.clear_cache() + self.env['sun.badge.menu.mixin'].sudo().trigger_menu_reload() + return res @api.depends('read_number') def _compute_read_number(self): @@ -134,38 +223,79 @@ class YuthonNotice(models.Model): self.sequence = 0 def notice_publish(self): - if self.is_change_notice: - conf_old = self.env['yuthon.confirm.users'].search([('notice_id', '=', self.id), ('employee_id', 'in', self.users_ids.ids)]) - for user_id in self.users_ids: - url = f'http://oa.thtzjt.com/webh5#/notice?id={self.id}' - if conf_old: - for co in conf_old: - co.write({'state': 'no'}) - else: - self.env['yuthon.confirm.users'].create({ - 'notice_id': self.id, - 'employee_id': user_id.employee_ids.id, - 'notice_code': self.code, - 'notice_name': self.name, - 'state': 'no', - }) - we_id = self.env['hr.employee'].search([('id', '=', user_id.employee_ids.id)]).wecom_userid - self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', user_id=we_id, title="通知公告", - description=self.name + '发布人:' + self.users_id.name, - url=url, btntxt="详细信息") - self.write({"notice_state": 'yes'}) - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'type': 'success', - 'message': '通知公告已经发布', - 'sticky': False, - } - } - else: - raise UserError('该公告不允许通知,请勾选通知') + """发布通知公告:给 yuthon.confirm.users 中尚未发送的记录发企微消息。 + - 每人有且仅发一条:msg_id 为空才发,已发过的不重发 + - 发送成功才写回 msg_id(企微返回的msgid)并更新状态为"待确认" + - 单条失败不影响其他记录,失败记录保持"待发送"状态 + """ + self.ensure_one() + + if self.notice_state == 'yes': + return + self.notice_state = 'yes' + + if not self.is_change_notice: + return + + # 重新发布时,已发送过消息的记录重置为"待确认",等待用户重新确认(不重发消息) + self.env['yuthon.confirm.users'].sudo().search([ + ('notice_id', '=', self.id), + ('msg_id', '!=', False), + ]).write({'state': 'no'}) + + # 只发送尚未收到消息的记录(msg_id 为空),保证每人有且仅一条 + confirm_records = self.env['yuthon.confirm.users'].sudo().search([ + ('notice_id', '=', self.id), + ('msg_id', '=', False), + ]) + + sent_count = 0 + fail_count = 0 + for rec in confirm_records: + employee = rec.employee_id + if not employee or not employee.wecom_userid: + _logger.warning("公告[%s]确认记录%s跳过:员工或企微userid为空", self.name, rec.id) + continue + url = f'https://phone.thtzjt.com/phone/Notice/{self.id}?employee_id={employee.id}' + try: + result_data = self.sudo().env["wecom.apps"].sync_send_message_textcard( + category='notice', + touser=employee.wecom_userid, + title="通知公告", + description=f"{self.name} 发布人: {self.users_id.name or ''}", + url=url, + btntxt="详细信息" + ) + except Exception: + _logger.exception("公告[%s]给员工%s发送企微消息异常", self.name, employee.name) + fail_count += 1 + continue + + # 发送成功:返回元组第一项为 True 且携带 msgid,才写回 msg_id 并更新状态 + if result_data and result_data[0] and isinstance(result_data[1], dict) and result_data[1].get('msgid'): + rec.sudo().write({ + 'msg_id': result_data[1].get('msgid'), + 'state': 'no', + }) + sent_count += 1 + else: + _logger.warning("公告[%s]给员工%s发送失败,返回:%s", self.name, employee.name, result_data) + fail_count += 1 + + _logger.info("公告[%s]发布完成,成功发送%s条,失败%s条", self.name, sent_count, fail_count) + + # 发布完毕弹窗提醒 + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '发布完毕', + 'message': f'已成功发送 {sent_count} 条消息' + (f',{fail_count} 条失败' if fail_count else ''), + 'type': 'success', + 'sticky': False, + }, + } def look_up_notice_users(self): return { @@ -179,28 +309,90 @@ class YuthonNotice(models.Model): }, } + def write(self, vals): + """重写write方法,检测work_end字段变化,触发AI主动感知""" + result = super().write(vals) + + # 检测work_end从False变为True + if 'work_end' in vals and vals['work_end']: + for record in self: + if record.work_end: # 确保当前值确实是True + self._trigger_ai_guide(record) + + return result + + def _trigger_ai_guide(self, record): + """触发AI主动感知:查找匹配的规则,推送消息到聊天框""" + # 查找匹配的规则 + rules = self.env['ai.rule'].sudo().search([ + ('rule_type', '=', 'active_guide'), + ('guide_model', '=', self._name), + ('guide_field', '=', 'work_end'), + ('guide_field_value', '=', 'True'), + ('active', '=', True), + ]) + + for rule in rules: + # 生成提醒内容(替换变量) + message = rule.guide_message or f'{record.name} 流程已结束' + message = message.replace('{record_name}', record.name or '') + message = message.replace('{user_name}', self.env.user.name or '') + + # 自动组装按钮动作(后台自动注入 record_id) + action_config = {} + if rule.guide_method_name: + action_config = { + 'type': 'execute_tool', + 'tool': rule.guide_method_name, + 'params': { + 'record_id': record.id, + 'model': self._name, + } + } + + # 查找或创建AI对话(用当前用户的默认对话) + conversation = self.env['ai.conversation'].sudo().search([ + ('create_uid', '=', self.env.user.id), + ], limit=1, order='write_date desc') + + if not conversation: + conversation = self.env['ai.conversation'].sudo().create({ + 'name': 'AI 对话', + }) + + # 创建AI消息(带action) + self.env['ai.message'].sudo().create({ + 'conversation_id': conversation.id, + 'role': 'assistant', + 'content': message, + 'action': json.dumps({ + 'type': 'guide_button', + 'label': rule.guide_button_label or '确认执行', + 'action': action_config, + }, ensure_ascii=False) if action_config else None, + }) + + # TODO: 通过bus.bus推送到前端,让聊天框自动显示新消息 + # self.env['bus.bus'].sudo()._sendone(self.env.user.partner_id, 'ai_guide_notification', { + # 'conversation_id': conversation.id, + # 'message': message, + # }) + def stop_notice(self): self.write({ 'notice_state': 'stop', 'end_date': fields.Date.today(), }) + def action_print_notice(self): + """打印通知公告""" + self.ensure_one() + report = self.env.ref('yuthon_notice.action_notice_print_report') + return report.report_action(self) + class YuthonCsTree(models.Model): _name = 'yuthon.cs.tree' _description = 'Yuthon Customer Service Tree' name = fields.Char(string='Name', required=True) - - def _compute_sequence(self): - for record in self: - record.sequence = 1 - - - - -# 工作流的指定申请人逻辑? -# 转交的时候选择条件,进行优化 - -# 报表合同和产品业务 -# 企业微信等三方平台框架--和工作流 \ No newline at end of file diff --git a/yuthon_notice/report/notice_print_report.xml b/yuthon_notice/report/notice_print_report.xml new file mode 100644 index 0000000..8753674 --- /dev/null +++ b/yuthon_notice/report/notice_print_report.xml @@ -0,0 +1,23 @@ + + + + + Notice Small Top Margin + A4 + 5 + Portrait + + + + 通知公告 + + yuthon.notice + qweb-pdf + yuthon_notice.notice_print_template + yuthon_notice.notice_print_template + + report + '通知公告_%s_%s' % (object.name or '', object.code or '') + + + diff --git a/yuthon_notice/report/notice_print_template.xml b/yuthon_notice/report/notice_print_template.xml new file mode 100644 index 0000000..75cce6a --- /dev/null +++ b/yuthon_notice/report/notice_print_template.xml @@ -0,0 +1,144 @@ + + + + diff --git a/yuthon_notice/static/scss/notice_scss.scss b/yuthon_notice/static/scss/notice_scss.scss index 0bcc2b2..cd056d7 100644 --- a/yuthon_notice/static/scss/notice_scss.scss +++ b/yuthon_notice/static/scss/notice_scss.scss @@ -2,4 +2,8 @@ .list_ids_width{ width: 80px !important; max-width: 80px !important; +} + +.btn-blue { + color: #0069ff !important; } \ No newline at end of file diff --git a/yuthon_notice/views/yuthon_confirm_users_views.xml b/yuthon_notice/views/yuthon_confirm_users_views.xml index 6cc06e4..53a3f7f 100644 --- a/yuthon_notice/views/yuthon_confirm_users_views.xml +++ b/yuthon_notice/views/yuthon_confirm_users_views.xml @@ -1,18 +1,26 @@ - - yuthon.confirm.users.tree + + yuthon.confirm.users.list yuthon.confirm.users - +
-
- + + + + + + + + + diff --git a/yuthon_will_task/views/task_template.xml b/yuthon_will_task/views/task_template.xml new file mode 100644 index 0000000..3762828 --- /dev/null +++ b/yuthon_will_task/views/task_template.xml @@ -0,0 +1,70 @@ + + + + \ No newline at end of file diff --git a/yuthon_will_task/views/yuthon_dates_task_views.xml b/yuthon_will_task/views/yuthon_dates_task_views.xml index 7a3c170..586b60a 100644 --- a/yuthon_will_task/views/yuthon_dates_task_views.xml +++ b/yuthon_will_task/views/yuthon_dates_task_views.xml @@ -1,10 +1,10 @@ - - yuthon.dates.task.tree + + yuthon.dates.task.list yuthon.dates.task - @@ -12,7 +12,7 @@ - +
@@ -30,8 +30,8 @@ 日程任务 yuthon.dates.task - tree - + list + {'search_default_task_state':1} diff --git a/yuthon_will_task/views/yuthon_task_menu.xml b/yuthon_will_task/views/yuthon_task_menu.xml index 7a54cb0..96d3705 100644 --- a/yuthon_will_task/views/yuthon_task_menu.xml +++ b/yuthon_will_task/views/yuthon_task_menu.xml @@ -4,11 +4,27 @@ name='会议日程' web_icon="yuthon_will_task,static/description/icon.png" sequence='10'/> + + + + + + + -
+ \ No newline at end of file diff --git a/yuthon_will_task/views/yuthon_task_time_views.xml b/yuthon_will_task/views/yuthon_task_time_views.xml index 5ddad4e..ec497d2 100644 --- a/yuthon_will_task/views/yuthon_task_time_views.xml +++ b/yuthon_will_task/views/yuthon_task_time_views.xml @@ -1,21 +1,23 @@ - - yuthon.task.time.tree + + yuthon.task.time.list yuthon.task.time - + - + 提醒时长配置 yuthon.task.time - tree + list + + diff --git a/yuthon_will_task/views/yuthon_will_task_views.xml b/yuthon_will_task/views/yuthon_will_task_views.xml index a47e339..8bdbea9 100644 --- a/yuthon_will_task/views/yuthon_will_task_views.xml +++ b/yuthon_will_task/views/yuthon_will_task_views.xml @@ -1,10 +1,10 @@ - - yuthon.will.task.tree + + yuthon.will.task.list yuthon.will.task - + + + + + + +