diff --git a/yuthon_salary/__init__.py b/yuthon_salary/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/yuthon_salary/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/yuthon_salary/__manifest__.py b/yuthon_salary/__manifest__.py new file mode 100644 index 0000000..6db0a3d --- /dev/null +++ b/yuthon_salary/__manifest__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +{ + 'name': '宇森教育薪资管理', + 'summary': '学校教育行业教职工薪资管理系统', + 'description': ''' + 专为学校教育行业设计的教职工薪资管理系统,主要功能包括: + + - 教职工薪资档案管理 + - 薪资结构配置(基本工资、岗位工资、绩效工资) + - 课时费管理(按科目、年级、职称差异化计费) + - 班主任津贴管理 + - 社保公积金扣缴配置 + - 个人所得税自动计算 + - 薪资单生成与审批 + - 薪资条发放与查询 + - 薪资报表统计分析 + + 适用于幼儿园、小学、中学、高中、职业院校等各类教育机构。 + ''', + 'author': '宇森科技', + 'version': '18.0.1.0', + 'category': 'Human Resources/Payroll', + 'sequence': 250, + 'depends': ['base', 'hr', 'hr_contract', 'mail', 'yuthon_base'], + 'data': [ + 'security/yuthon_salary_groups.xml', + 'security/ir.model.access.csv', + 'security/yuthon_salary_security.xml', + 'data/salary_item_data.xml', + 'views/salary_item_views.xml', + 'views/salary_structure_views.xml', + 'views/employee_salary_views.xml', + 'views/class_hour_views.xml', + 'views/class_teacher_allowance_views.xml', + 'views/salary_slip_views.xml', + 'views/salary_report_views.xml', + 'views/menu_views.xml', + ], + 'installable': True, + 'application': True, + 'license': 'LGPL-3', +} diff --git a/yuthon_salary/data/salary_item_data.xml b/yuthon_salary/data/salary_item_data.xml new file mode 100644 index 0000000..61bf3de --- /dev/null +++ b/yuthon_salary/data/salary_item_data.xml @@ -0,0 +1,152 @@ + + + + + + 基本工资 + BASIC + basic + income + fixed + 10 + + + + 岗位工资 + POSITION + position + income + fixed + 20 + + + + 绩效工资 + PERFORMANCE + performance + income + formula + 30 + + + + 课时费 + CLASS_HOUR + class_hour + income + formula + 40 + + + + 班主任津贴 + CLASS_TEACHER + allowance + income + manual + 50 + + + + 工龄津贴 + SENIORITY + allowance + income + fixed + 60 + + + + 学历津贴 + EDUCATION + allowance + income + fixed + 70 + + + + 职称津贴 + TITLE + allowance + income + fixed + 80 + + + + 养老保险(个人) + PENSION + social + deduction + formula + 110 + + + + 医疗保险(个人) + MEDICAL + social + deduction + formula + 120 + + + + 失业保险(个人) + UNEMPLOYMENT + social + deduction + formula + 130 + + + + 住房公积金(个人) + PROVIDENT_FUND + social + deduction + formula + 140 + + + + 个人所得税 + INCOME_TAX + tax + deduction + formula + True + 150 + + + + + 教职工薪资档案编号 + yuthon.employee.salary + ESA%(year)s + 4 + + + + 课时记录编号 + yuthon.class.hour + CH%(year)s + 4 + + + + 班主任津贴编号 + yuthon.class.teacher.allowance + CTA%(year)s%(month)s + 3 + + + + 薪资单编号 + yuthon.salary.slip + SLIP%(year)s%(month)s + 3 + + + diff --git a/yuthon_salary/models/.gitkeep b/yuthon_salary/models/.gitkeep new file mode 100644 index 0000000..867e2c8 --- /dev/null +++ b/yuthon_salary/models/.gitkeep @@ -0,0 +1 @@ +# Placeholder \ No newline at end of file diff --git a/yuthon_salary/models/__init__.py b/yuthon_salary/models/__init__.py new file mode 100644 index 0000000..f8cbd3c --- /dev/null +++ b/yuthon_salary/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from . import salary_item +from . import salary_structure +from . import employee_salary +from . import class_hour +from . import class_teacher_allowance +from . import salary_slip diff --git a/yuthon_salary/models/class_hour.py b/yuthon_salary/models/class_hour.py new file mode 100644 index 0000000..1125ba5 --- /dev/null +++ b/yuthon_salary/models/class_hour.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class ClassHour(models.Model): + """课时记录 - 记录教职工的授课课时""" + _name = 'yuthon.class.hour' + _description = '课时记录' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'date desc, id desc' + + name = fields.Char(string='课时编号', required=True, copy=False, readonly=True, default='新建') + employee_id = fields.Many2one('hr.employee', string='授课教师', required=True) + department_id = fields.Many2one('hr.department', string='所属部门', related='employee_id.department_id', store=True) + + # 授课信息 + date = fields.Date(string='授课日期', required=True, default=fields.Date.today) + subject = fields.Char(string='科目', required=True) + grade = fields.Char(string='年级/班级') + class_type = fields.Selection([ + ('normal', '常规课'), + ('overtime', '超课时'), + ('weekend', '周末课'), + ('holiday', '节假日课'), + ('makeup', '补课'), + ('exam', '考试监考'), + ('activity', '课外活动'), + ], string='课程类型', required=True, default='normal') + + # 课时计算 + hours = fields.Float(string='课时数', required=True, default=1.0) + rate = fields.Float(string='课时费标准(元/课时)', required=True, default=0.0) + amount = fields.Float(string='课时费金额', compute='_compute_amount', store=True) + + # 状态 + state = fields.Selection([ + ('draft', '草稿'), + ('submitted', '已提交'), + ('approved', '已确认'), + ('paid', '已发薪'), + ], string='状态', default='draft', tracking=True) + + # 关联薪资单 + salary_slip_id = fields.Many2one('yuthon.salary.slip', string='关联薪资单', readonly=True) + + note = fields.Text(string='备注') + company_id = fields.Many2one('res.company', string='学校', default=lambda self: self.env.company) + + @api.depends('hours', 'rate') + def _compute_amount(self): + for record in self: + record.amount = record.hours * record.rate + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', '新建') == '新建': + vals['name'] = self.env['ir.sequence'].next_by_code('yuthon.class.hour') or '新建' + return super(ClassHour, self).create(vals_list) + + def action_submit(self): + self.write({'state': 'submitted'}) + + def action_approve(self): + self.write({'state': 'approved'}) + + def action_draft(self): + self.write({'state': 'draft'}) + + @api.onchange('employee_id', 'class_type') + def _onchange_employee_class_type(self): + if self.employee_id and self.employee_id.user_id: + salary_record = self.env['yuthon.employee.salary'].search([ + ('employee_id', '=', self.employee_id.id), + ('state', '=', 'active') + ], limit=1, order='id desc') + if salary_record: + if self.class_type == 'normal': + self.rate = salary_record.class_hour_rate_normal + elif self.class_type == 'overtime': + self.rate = salary_record.class_hour_rate_overtime + elif self.class_type == 'weekend': + self.rate = salary_record.class_hour_rate_weekend + elif self.class_type == 'holiday': + self.rate = salary_record.class_hour_rate_holiday diff --git a/yuthon_salary/models/class_teacher_allowance.py b/yuthon_salary/models/class_teacher_allowance.py new file mode 100644 index 0000000..90f27c1 --- /dev/null +++ b/yuthon_salary/models/class_teacher_allowance.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class ClassTeacherAllowance(models.Model): + """班主任津贴 - 管理班主任的月度津贴""" + _name = 'yuthon.class.teacher.allowance' + _description = '班主任津贴' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'year desc, month desc, id desc' + + name = fields.Char(string='津贴编号', required=True, copy=False, readonly=True, default='新建') + employee_id = fields.Many2one('hr.employee', string='班主任', required=True) + department_id = fields.Many2one('hr.department', string='所属部门', related='employee_id.department_id', store=True) + + # 班级信息 + class_name = fields.Char(string='班级名称', required=True) + grade_level = fields.Selection([ + ('junior', '低年级'), + ('middle', '中年级'), + ('senior', '高年级'), + ('junior_high', '初中'), + ('senior_high', '高中'), + ], string='年级段') + student_count = fields.Integer(string='学生人数', default=0) + + # 考核评分 + year = fields.Integer(string='年度', required=True, default=lambda self: fields.Date.today().year) + month = fields.Integer(string='月份', required=True, default=lambda self: fields.Date.today().month) + + # 津贴构成 + base_allowance = fields.Float(string='基础津贴', required=True, default=0.0) + student_count_bonus = fields.Float(string='学生人数补贴', default=0.0) + performance_bonus = fields.Float(string='绩效奖金', default=0.0) + special_allowance = fields.Float(string='特殊补贴', default=0.0) + deduction = fields.Float(string='扣款', default=0.0) + + total_amount = fields.Float(string='津贴合计', compute='_compute_total', store=True) + + # 考核维度 + discipline_score = fields.Float(string='纪律管理评分', default=100.0) + hygiene_score = fields.Float(string='卫生管理评分', default=100.0) + activity_score = fields.Float(string='活动组织评分', default=100.0) + parent_score = fields.Float(string='家长沟通评分', default=100.0) + overall_score = fields.Float(string='综合评分', compute='_compute_score', store=True) + + # 状态 + state = fields.Selection([ + ('draft', '草稿'), + ('submitted', '已提交'), + ('approved', '已审核'), + ('paid', '已发薪'), + ], string='状态', default='draft', tracking=True) + + salary_slip_id = fields.Many2one('yuthon.salary.slip', string='关联薪资单', readonly=True) + note = fields.Text(string='备注') + company_id = fields.Many2one('res.company', string='学校', default=lambda self: self.env.company) + + @api.depends('base_allowance', 'student_count_bonus', 'performance_bonus', 'special_allowance', 'deduction') + def _compute_total(self): + for record in self: + record.total_amount = ( + record.base_allowance + + record.student_count_bonus + + record.performance_bonus + + record.special_allowance - + record.deduction + ) + + @api.depends('discipline_score', 'hygiene_score', 'activity_score', 'parent_score') + def _compute_score(self): + for record in self: + record.overall_score = ( + record.discipline_score + + record.hygiene_score + + record.activity_score + + record.parent_score + ) / 4.0 + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', '新建') == '新建': + vals['name'] = self.env['ir.sequence'].next_by_code('yuthon.class.teacher.allowance') or '新建' + return super(ClassTeacherAllowance, self).create(vals_list) + + def action_submit(self): + self.write({'state': 'submitted'}) + + def action_approve(self): + self.write({'state': 'approved'}) + + def action_draft(self): + self.write({'state': 'draft'}) + + @api.onchange('employee_id') + def _onchange_employee(self): + if self.employee_id: + salary_record = self.env['yuthon.employee.salary'].search([ + ('employee_id', '=', self.employee_id.id), + ('state', '=', 'active') + ], limit=1, order='id desc') + if salary_record: + self.base_allowance = salary_record.class_teacher_allowance diff --git a/yuthon_salary/models/employee_salary.py b/yuthon_salary/models/employee_salary.py new file mode 100644 index 0000000..aa75962 --- /dev/null +++ b/yuthon_salary/models/employee_salary.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class EmployeeSalary(models.Model): + """教职工薪资档案 - 记录每位教职工的薪资配置""" + _name = 'yuthon.employee.salary' + _description = '教职工薪资档案' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'employee_id' + + name = fields.Char(string='档案编号', required=True, copy=False, readonly=True, default='新建') + employee_id = fields.Many2one('hr.employee', string='教职工', required=True) + department_id = fields.Many2one('hr.department', string='所属部门', related='employee_id.department_id', store=True) + job_id = fields.Many2one('hr.job', string='岗位', related='employee_id.job_id', store=True) + + # 基本信息 + employee_type = fields.Selection([ + ('full_time', '正式编制'), + ('contract', '合同制'), + ('temporary', '临聘'), + ('intern', '实习'), + ('part_time', '兼职'), + ], string='用工类型', required=True, default='full_time') + teacher_title = fields.Selection([ + ('none', '无职称'), + ('third', '三级教师'), + ('second', '二级教师'), + ('first', '一级教师'), + ('senior', '高级教师'), + ('special', '特级教师'), + ('lecturer', '讲师'), + ('associate_prof', '副教授'), + ('professor', '教授'), + ], string='教师职称', default='none') + education_level = fields.Selection([ + ('college', '大专'), + ('bachelor', '本科'), + ('master', '硕士'), + ('doctor', '博士'), + ], string='学历层次', default='bachelor') + + # 薪资结构 + structure_id = fields.Many2one('yuthon.salary.structure', string='适用薪资结构') + + # 基本工资 + basic_salary = fields.Float(string='基本工资', required=True, default=0.0, tracking=True) + position_salary = fields.Float(string='岗位工资', default=0.0, tracking=True) + performance_salary = fields.Float(string='绩效工资基数', default=0.0, tracking=True) + + # 课时费标准(按职称/科目差异化) + class_hour_rate_normal = fields.Float(string='常规课时费标准(元/课时)', default=0.0) + class_hour_rate_overtime = fields.Float(string='超课时费标准(元/课时)', default=0.0) + class_hour_rate_weekend = fields.Float(string='周末课时费标准(元/课时)', default=0.0) + class_hour_rate_holiday = fields.Float(string='节假日课时费标准(元/课时)', default=0.0) + + # 班主任津贴 + is_class_teacher = fields.Boolean(string='是否班主任', default=False) + class_teacher_allowance = fields.Float(string='班主任津贴', default=0.0) + class_level = fields.Selection([ + ('junior', '低年级'), + ('middle', '中年级'), + ('senior', '高年级'), + ('junior_high', '初中'), + ('senior_high', '高中'), + ], string='任教年级段') + + # 其他固定津贴 + seniority_allowance = fields.Float(string='工龄津贴', default=0.0) + education_allowance = fields.Float(string='学历津贴', default=0.0) + title_allowance = fields.Float(string='职称津贴', default=0.0) + other_allowance = fields.Float(string='其他津贴', default=0.0) + + # 社保公积金 + social_insurance_base = fields.Float(string='社保基数', default=0.0) + provident_fund_base = fields.Float(string='公积金基数', default=0.0) + pension_rate_personal = fields.Float(string='养老保险个人比例(%)', default=8.0) + medical_rate_personal = fields.Float(string='医疗保险个人比例(%)', default=2.0) + unemployment_rate_personal = fields.Float(string='失业保险个人比例(%)', default=0.5) + provident_fund_rate_personal = fields.Float(string='公积金个人比例(%)', default=12.0) + + # 个税 + tax_deduction_point = fields.Float(string='个税起征点', default=5000.0) + special_deduction = fields.Float(string='专项附加扣除', default=0.0) + + # 银行卡信息 + bank_name = fields.Char(string='开户银行') + bank_account = fields.Char(string='银行卡号') + bank_branch = fields.Char(string='开户支行') + + # 状态 + state = fields.Selection([ + ('draft', '草稿'), + ('active', '生效'), + ('suspended', '停薪'), + ('resigned', '离职'), + ], string='状态', default='draft', tracking=True) + + effective_date = fields.Date(string='生效日期', default=fields.Date.today) + note = fields.Text(string='备注') + company_id = fields.Many2one('res.company', string='学校', default=lambda self: self.env.company) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', '新建') == '新建': + vals['name'] = self.env['ir.sequence'].next_by_code('yuthon.employee.salary') or '新建' + return super(EmployeeSalary, self).create(vals_list) + + def action_activate(self): + self.write({'state': 'active'}) + + def action_suspend(self): + self.write({'state': 'suspended'}) + + def action_resign(self): + self.write({'state': 'resigned'}) diff --git a/yuthon_salary/models/salary_item.py b/yuthon_salary/models/salary_item.py new file mode 100644 index 0000000..37d0ed5 --- /dev/null +++ b/yuthon_salary/models/salary_item.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class SalaryItem(models.Model): + """薪资项目 - 定义薪资单中的各个项目""" + _name = 'yuthon.salary.item' + _description = '薪资项目' + _order = 'sequence, id' + + name = fields.Char(string='项目名称', required=True) + code = fields.Char(string='项目编码', required=True) + sequence = fields.Integer(string='排序', default=10) + category = fields.Selection([ + ('basic', '基本工资'), + ('position', '岗位工资'), + ('performance', '绩效工资'), + ('class_hour', '课时费'), + ('allowance', '津贴补贴'), + ('deduction', '扣款项'), + ('social', '社保公积金'), + ('tax', '个人所得税'), + ('other_income', '其他收入'), + ('other_deduction', '其他扣款'), + ], string='项目类别', required=True) + item_type = fields.Selection([ + ('income', '收入项'), + ('deduction', '扣款项'), + ], string='项目类型', required=True, default='income') + calculation_method = fields.Selection([ + ('fixed', '固定金额'), + ('formula', '公式计算'), + ('manual', '手动录入'), + ], string='计算方式', required=True, default='fixed') + formula = fields.Text(string='计算公式') + is_taxable = fields.Boolean(string='是否计税', default=False) + is_social_base = fields.Boolean(string='是否社保基数', default=False) + is_required = fields.Boolean(string='必填项', default=False) + active = fields.Boolean(string='启用', default=True) + note = fields.Text(string='备注') + + _sql_constraints = [ + ('code_uniq', 'unique(code)', '项目编码必须唯一!'), + ] diff --git a/yuthon_salary/models/salary_slip.py b/yuthon_salary/models/salary_slip.py new file mode 100644 index 0000000..a367add --- /dev/null +++ b/yuthon_salary/models/salary_slip.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class SalarySlip(models.Model): + """薪资单 - 教职工月度薪资发放记录""" + _name = 'yuthon.salary.slip' + _description = '薪资单' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'year desc, month desc, employee_id' + + name = fields.Char(string='薪资单编号', required=True, copy=False, readonly=True, default='新建') + employee_id = fields.Many2one('hr.employee', string='教职工', required=True) + department_id = fields.Many2one('hr.department', string='所属部门', related='employee_id.department_id', store=True) + job_id = fields.Many2one('hr.job', string='岗位', related='employee_id.job_id', store=True) + + # 薪资周期 + year = fields.Integer(string='年度', required=True, default=lambda self: fields.Date.today().year) + month = fields.Integer(string='月份', required=True, default=lambda self: fields.Date.today().month) + date_from = fields.Date(string='开始日期', required=True) + date_to = fields.Date(string='结束日期', required=True) + + # 关联档案 + salary_record_id = fields.Many2one('yuthon.employee.salary', string='薪资档案') + + # 收入项 + basic_salary = fields.Float(string='基本工资', default=0.0) + position_salary = fields.Float(string='岗位工资', default=0.0) + performance_salary = fields.Float(string='绩效工资', default=0.0) + class_hour_fee = fields.Float(string='课时费', default=0.0) + class_teacher_allowance = fields.Float(string='班主任津贴', default=0.0) + seniority_allowance = fields.Float(string='工龄津贴', default=0.0) + education_allowance = fields.Float(string='学历津贴', default=0.0) + title_allowance = fields.Float(string='职称津贴', default=0.0) + other_income = fields.Float(string='其他收入', default=0.0) + + # 扣款项 + pension_personal = fields.Float(string='养老保险(个人)', default=0.0) + medical_personal = fields.Float(string='医疗保险(个人)', default=0.0) + unemployment_personal = fields.Float(string='失业保险(个人)', default=0.0) + provident_fund_personal = fields.Float(string='公积金(个人)', default=0.0) + other_deduction = fields.Float(string='其他扣款', default=0.0) + + # 个税 + taxable_income = fields.Float(string='应纳税所得额', compute='_compute_totals', store=True) + personal_income_tax = fields.Float(string='个人所得税', compute='_compute_tax', store=True) + + # 合计 + total_income = fields.Float(string='应发合计', compute='_compute_totals', store=True) + total_deduction = fields.Float(string='扣款合计', compute='_compute_totals', store=True) + net_salary = fields.Float(string='实发工资', compute='_compute_totals', store=True) + + # 银行信息 + bank_name = fields.Char(string='开户银行') + bank_account = fields.Char(string='银行卡号') + + # 状态 + state = fields.Selection([ + ('draft', '草稿'), + ('confirmed', '已确认'), + ('approved', '已审核'), + ('paid', '已发放'), + ], string='状态', default='draft', tracking=True) + + line_ids = fields.One2many('yuthon.salary.slip.line', 'slip_id', string='薪资明细') + note = fields.Text(string='备注') + company_id = fields.Many2one('res.company', string='学校', default=lambda self: self.env.company) + + @api.depends('basic_salary', 'position_salary', 'performance_salary', 'class_hour_fee', + 'class_teacher_allowance', 'seniority_allowance', 'education_allowance', + 'title_allowance', 'other_income', + 'pension_personal', 'medical_personal', 'unemployment_personal', + 'provident_fund_personal', 'other_deduction', 'personal_income_tax') + def _compute_totals(self): + for slip in self: + income = ( + slip.basic_salary + slip.position_salary + slip.performance_salary + + slip.class_hour_fee + slip.class_teacher_allowance + + slip.seniority_allowance + slip.education_allowance + + slip.title_allowance + slip.other_income + ) + deduction = ( + slip.pension_personal + slip.medical_personal + + slip.unemployment_personal + slip.provident_fund_personal + + slip.other_deduction + slip.personal_income_tax + ) + slip.total_income = income + slip.total_deduction = deduction + slip.taxable_income = max(0, income - deduction + slip.personal_income_tax - 5000) + slip.net_salary = income - deduction + + @api.depends('taxable_income') + def _compute_tax(self): + """简化版个税计算(中国个税累进税率)""" + for slip in self: + taxable = slip.taxable_income + if taxable <= 0: + slip.personal_income_tax = 0 + elif taxable <= 3000: + slip.personal_income_tax = taxable * 0.03 + elif taxable <= 12000: + slip.personal_income_tax = taxable * 0.10 - 210 + elif taxable <= 25000: + slip.personal_income_tax = taxable * 0.20 - 1410 + elif taxable <= 35000: + slip.personal_income_tax = taxable * 0.25 - 2660 + elif taxable <= 55000: + slip.personal_income_tax = taxable * 0.30 - 4410 + elif taxable <= 80000: + slip.personal_income_tax = taxable * 0.35 - 7160 + else: + slip.personal_income_tax = taxable * 0.45 - 15160 + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', '新建') == '新建': + vals['name'] = self.env['ir.sequence'].next_by_code('yuthon.salary.slip') or '新建' + return super(SalarySlip, self).create(vals_list) + + def action_calculate(self): + """自动计算薪资""" + for slip in self: + if slip.salary_record_id: + record = slip.salary_record_id + slip.basic_salary = record.basic_salary + slip.position_salary = record.position_salary + slip.performance_salary = record.performance_salary + slip.seniority_allowance = record.seniority_allowance + slip.education_allowance = record.education_allowance + slip.title_allowance = record.title_allowance + slip.bank_name = record.bank_name + slip.bank_account = record.bank_account + + # 计算社保 + if record.social_insurance_base > 0: + slip.pension_personal = record.social_insurance_base * record.pension_rate_personal / 100 + slip.medical_personal = record.social_insurance_base * record.medical_rate_personal / 100 + slip.unemployment_personal = record.social_insurance_base * record.unemployment_rate_personal / 100 + if record.provident_fund_base > 0: + slip.provident_fund_personal = record.provident_fund_base * record.provident_fund_rate_personal / 100 + + # 统计课时费 + class_hours = self.env['yuthon.class.hour'].search([ + ('employee_id', '=', slip.employee_id.id), + ('date', '>=', slip.date_from), + ('date', '<=', slip.date_to), + ('state', 'in', ['approved', 'paid']), + ]) + slip.class_hour_fee = sum(ch.amount for ch in class_hours) + + # 统计班主任津贴 + allowances = self.env['yuthon.class.teacher.allowance'].search([ + ('employee_id', '=', slip.employee_id.id), + ('year', '=', slip.year), + ('month', '=', slip.month), + ('state', 'in', ['approved', 'paid']), + ]) + slip.class_teacher_allowance = sum(a.total_amount for a in allowances) + + slip._compute_totals() + slip._compute_tax() + + def action_confirm(self): + self.write({'state': 'confirmed'}) + + def action_approve(self): + self.write({'state': 'approved'}) + + def action_pay(self): + self.write({'state': 'paid'}) + + def action_draft(self): + self.write({'state': 'draft'}) + + @api.constrains('year', 'month') + def _check_year_month(self): + for slip in self: + if slip.month < 1 or slip.month > 12: + raise ValidationError(_('月份必须在 1-12 之间!')) + if slip.year < 2000 or slip.year > 2100: + raise ValidationError(_('年度格式不正确!')) + + +class SalarySlipLine(models.Model): + """薪资单明细行""" + _name = 'yuthon.salary.slip.line' + _description = '薪资单明细' + _order = 'sequence, id' + + slip_id = fields.Many2one('yuthon.salary.slip', string='薪资单', required=True, ondelete='cascade') + sequence = fields.Integer(string='排序', default=10) + item_id = fields.Many2one('yuthon.salary.item', string='薪资项目') + name = fields.Char(string='项目名称') + item_type = fields.Selection([ + ('income', '收入'), + ('deduction', '扣款'), + ], string='类型') + amount = fields.Float(string='金额', default=0.0) + note = fields.Char(string='备注') diff --git a/yuthon_salary/models/salary_structure.py b/yuthon_salary/models/salary_structure.py new file mode 100644 index 0000000..4c24c3c --- /dev/null +++ b/yuthon_salary/models/salary_structure.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + + +class SalaryStructure(models.Model): + """薪资结构 - 定义不同岗位/职称的薪资构成""" + _name = 'yuthon.salary.structure' + _description = '薪资结构' + _order = 'name' + + name = fields.Char(string='结构名称', required=True) + code = fields.Char(string='结构编码', required=True) + description = fields.Text(string='说明') + # 适用对象 + applicable_type = fields.Selection([ + ('teacher', '任课教师'), + ('admin', '行政人员'), + ('leadership', '领导班子'), + ('support', '后勤人员'), + ('intern', '实习教师'), + ('all', '全体适用'), + ], string='适用类型', required=True, default='teacher') + # 适用学校类型 + school_type = fields.Selection([ + ('kindergarten', '幼儿园'), + ('primary', '小学'), + ('junior', '初中'), + ('senior', '高中'), + ('vocational', '职业院校'), + ('university', '高等院校'), + ('all', '全部适用'), + ], string='适用学校类型', required=True, default='all') + line_ids = fields.One2many('yuthon.salary.structure.line', 'structure_id', string='薪资项目明细') + active = fields.Boolean(string='启用', default=True) + company_id = fields.Many2one('res.company', string='学校', default=lambda self: self.env.company) + + # 合计字段 + total_basic = fields.Float(string='基本收入合计', compute='_compute_totals', store=True) + total_deduction = fields.Float(string='扣款合计', compute='_compute_totals', store=True) + total_net = fields.Float(string='实发合计', compute='_compute_totals', store=True) + + @api.depends('line_ids.amount', 'line_ids.item_id.item_type') + def _compute_totals(self): + for structure in self: + income = sum(line.amount for line in structure.line_ids if line.item_id.item_type == 'income') + deduction = sum(line.amount for line in structure.line_ids if line.item_id.item_type == 'deduction') + structure.total_basic = income + structure.total_deduction = deduction + structure.total_net = income - deduction + + _sql_constraints = [ + ('code_uniq', 'unique(code)', '结构编码必须唯一!'), + ] + + +class SalaryStructureLine(models.Model): + """薪资结构明细行""" + _name = 'yuthon.salary.structure.line' + _description = '薪资结构明细' + _order = 'sequence, id' + + structure_id = fields.Many2one('yuthon.salary.structure', string='薪资结构', required=True, ondelete='cascade') + sequence = fields.Integer(string='排序', default=10) + item_id = fields.Many2one('yuthon.salary.item', string='薪资项目', required=True) + amount = fields.Float(string='默认金额', default=0.0) + note = fields.Char(string='备注') diff --git a/yuthon_salary/security/ir.model.access.csv b/yuthon_salary/security/ir.model.access.csv new file mode 100644 index 0000000..4d32468 --- /dev/null +++ b/yuthon_salary/security/ir.model.access.csv @@ -0,0 +1,19 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_salary_item_user,yuthon.salary.item.user,model_yuthon_salary_item,base.group_user,1,0,0,0 +access_salary_item_manager,yuthon.salary.item.manager,model_yuthon_salary_item,yuthon_salary.group_manager,1,1,1,1 +access_salary_structure_user,yuthon.salary.structure.user,model_yuthon_salary_structure,base.group_user,1,0,0,0 +access_salary_structure_manager,yuthon.salary.structure.manager,model_yuthon_salary_structure,yuthon_salary.group_manager,1,1,1,1 +access_salary_structure_line_user,yuthon.salary.structure.line.user,model_yuthon_salary_structure_line,base.group_user,1,0,0,0 +access_salary_structure_line_manager,yuthon.salary.structure.line.manager,model_yuthon_salary_structure_line,yuthon_salary.group_manager,1,1,1,1 +access_employee_salary_user,yuthon.employee.salary.user,model_yuthon_employee_salary,base.group_user,1,0,0,0 +access_employee_salary_manager,yuthon.employee.salary.manager,model_yuthon_employee_salary,yuthon_salary.group_manager,1,1,1,1 +access_employee_salary_hr,yuthon.employee.salary.hr,model_yuthon_employee_salary,yuthon_salary.group_hr,1,1,1,0 +access_class_hour_user,yuthon.class.hour.user,model_yuthon_class_hour,base.group_user,1,1,1,0 +access_class_hour_manager,yuthon.class.hour.manager,model_yuthon_class_hour,yuthon_salary.group_manager,1,1,1,1 +access_class_teacher_allowance_user,yuthon.class.teacher.allowance.user,model_yuthon_class_teacher_allowance,base.group_user,1,1,1,0 +access_class_teacher_allowance_manager,yuthon.class.teacher.allowance.manager,model_yuthon_class_teacher_allowance,yuthon_salary.group_manager,1,1,1,1 +access_salary_slip_user,yuthon.salary.slip.user,model_yuthon_salary_slip,base.group_user,1,0,0,0 +access_salary_slip_manager,yuthon.salary.slip.manager,model_yuthon_salary_slip,yuthon_salary.group_manager,1,1,1,1 +access_salary_slip_hr,yuthon.salary.slip.hr,model_yuthon_salary_slip,yuthon_salary.group_hr,1,1,1,0 +access_salary_slip_line_user,yuthon.salary.slip.line.user,model_yuthon_salary_slip_line,base.group_user,1,0,0,0 +access_salary_slip_line_manager,yuthon.salary.slip.line.manager,model_yuthon_salary_slip_line,yuthon_salary.group_manager,1,1,1,1 diff --git a/yuthon_salary/security/yuthon_salary_groups.xml b/yuthon_salary/security/yuthon_salary_groups.xml new file mode 100644 index 0000000..8df8a1d --- /dev/null +++ b/yuthon_salary/security/yuthon_salary_groups.xml @@ -0,0 +1,17 @@ + + + + + + 薪资专员 + + + + + + 薪资管理员 + + + + + diff --git a/yuthon_salary/security/yuthon_salary_security.xml b/yuthon_salary/security/yuthon_salary_security.xml new file mode 100644 index 0000000..bc2e0d6 --- /dev/null +++ b/yuthon_salary/security/yuthon_salary_security.xml @@ -0,0 +1,50 @@ + + + + + + 教职工薪资档案:多学校隔离 + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + 薪资单:多学校隔离 + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + 课时记录:多学校隔离 + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + 班主任津贴:多学校隔离 + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + + 教职工只能查看自己的薪资单 + + + [('employee_id.user_id', '=', user.id)] + + + + + 教职工只能查看自己的课时记录 + + + [('employee_id.user_id', '=', user.id)] + + + + diff --git a/yuthon_salary/views/class_hour_views.xml b/yuthon_salary/views/class_hour_views.xml new file mode 100644 index 0000000..8be45fb --- /dev/null +++ b/yuthon_salary/views/class_hour_views.xml @@ -0,0 +1,87 @@ + + + + yuthon.class.hour.tree + yuthon.class.hour + + + + + + + + + + + + + + + + + + yuthon.class.hour.form + yuthon.class.hour + +
+
+
+ +
+

+
+ + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + yuthon.class.hour.search + yuthon.class.hour + + + + + + + + + + + + + + + + + + 课时记录 + yuthon.class.hour + list,form + +
diff --git a/yuthon_salary/views/class_teacher_allowance_views.xml b/yuthon_salary/views/class_teacher_allowance_views.xml new file mode 100644 index 0000000..1000f35 --- /dev/null +++ b/yuthon_salary/views/class_teacher_allowance_views.xml @@ -0,0 +1,106 @@ + + + + yuthon.class.teacher.allowance.tree + yuthon.class.teacher.allowance + + + + + + + + + + + + + + + + + yuthon.class.teacher.allowance.form + yuthon.class.teacher.allowance + +
+
+
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + yuthon.class.teacher.allowance.search + yuthon.class.teacher.allowance + + + + + + + + + + + + + + + + + 班主任津贴 + yuthon.class.teacher.allowance + list,form + +
diff --git a/yuthon_salary/views/employee_salary_views.xml b/yuthon_salary/views/employee_salary_views.xml new file mode 100644 index 0000000..58e5793 --- /dev/null +++ b/yuthon_salary/views/employee_salary_views.xml @@ -0,0 +1,138 @@ + + + + yuthon.employee.salary.tree + yuthon.employee.salary + + + + + + + + + + + + + + + yuthon.employee.salary.form + yuthon.employee.salary + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + yuthon.employee.salary.search + yuthon.employee.salary + + + + + + + + + + + + + + + + + + 教职工薪资档案 + yuthon.employee.salary + list,form + +
diff --git a/yuthon_salary/views/menu_views.xml b/yuthon_salary/views/menu_views.xml new file mode 100644 index 0000000..794017b --- /dev/null +++ b/yuthon_salary/views/menu_views.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yuthon_salary/views/salary_item_views.xml b/yuthon_salary/views/salary_item_views.xml new file mode 100644 index 0000000..7133c60 --- /dev/null +++ b/yuthon_salary/views/salary_item_views.xml @@ -0,0 +1,71 @@ + + + + yuthon.salary.item.tree + yuthon.salary.item + + + + + + + + + + + + + + + + yuthon.salary.item.form + yuthon.salary.item + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + yuthon.salary.item.search + yuthon.salary.item + + + + + + + + + + + + + + + + 薪资项目 + yuthon.salary.item + list,form + +
diff --git a/yuthon_salary/views/salary_report_views.xml b/yuthon_salary/views/salary_report_views.xml new file mode 100644 index 0000000..395fdc1 --- /dev/null +++ b/yuthon_salary/views/salary_report_views.xml @@ -0,0 +1,28 @@ + + + + yuthon.salary.slip.pivot + yuthon.salary.slip + + + + + + + + + + + + + + yuthon.salary.slip.graph + yuthon.salary.slip + + + + + + + + diff --git a/yuthon_salary/views/salary_slip_views.xml b/yuthon_salary/views/salary_slip_views.xml new file mode 100644 index 0000000..84c43fd --- /dev/null +++ b/yuthon_salary/views/salary_slip_views.xml @@ -0,0 +1,137 @@ + + + + yuthon.salary.slip.tree + yuthon.salary.slip + + + + + + + + + + + + + + + + + yuthon.salary.slip.form + yuthon.salary.slip + +
+
+
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + yuthon.salary.slip.search + yuthon.salary.slip + + + + + + + + + + + + + + + + + + + + 薪资单 + yuthon.salary.slip + list,form + + + + + 薪资报表 + yuthon.salary.slip + list,form,pivot,graph + +
diff --git a/yuthon_salary/views/salary_structure_views.xml b/yuthon_salary/views/salary_structure_views.xml new file mode 100644 index 0000000..edac514 --- /dev/null +++ b/yuthon_salary/views/salary_structure_views.xml @@ -0,0 +1,63 @@ + + + + yuthon.salary.structure.tree + yuthon.salary.structure + + + + + + + + + + + + + + + + yuthon.salary.structure.form + yuthon.salary.structure + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + 薪资结构 + yuthon.salary.structure + list,form + +