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 0000000..7c14d2c Binary files /dev/null and b/yuthon_hr_employee/static/description/icon.png differ 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 0000000..2c4f3b2 Binary files /dev/null and b/yuthon_hr_employee/static/others/user_template.xlsx differ 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 - + + + + + + +