From e9d4066e45094b59345d958684af3688ce080091 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?=
<9664676+pengyuthon@user.noreply.gitee.com>
Date: Thu, 30 Jul 2026 19:56:43 +0800
Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=BF=81=E7=A7=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
yuthon_salary/__init__.py | 2 +
yuthon_salary/__manifest__.py | 42 ++++
yuthon_salary/data/salary_item_data.xml | 152 +++++++++++++
yuthon_salary/models/.gitkeep | 1 +
yuthon_salary/models/__init__.py | 7 +
yuthon_salary/models/class_hour.py | 85 ++++++++
.../models/class_teacher_allowance.py | 104 +++++++++
yuthon_salary/models/employee_salary.py | 117 ++++++++++
yuthon_salary/models/salary_item.py | 44 ++++
yuthon_salary/models/salary_slip.py | 201 ++++++++++++++++++
yuthon_salary/models/salary_structure.py | 66 ++++++
yuthon_salary/security/ir.model.access.csv | 19 ++
.../security/yuthon_salary_groups.xml | 17 ++
.../security/yuthon_salary_security.xml | 50 +++++
yuthon_salary/views/class_hour_views.xml | 87 ++++++++
.../views/class_teacher_allowance_views.xml | 106 +++++++++
yuthon_salary/views/employee_salary_views.xml | 138 ++++++++++++
yuthon_salary/views/menu_views.xml | 68 ++++++
yuthon_salary/views/salary_item_views.xml | 71 +++++++
yuthon_salary/views/salary_report_views.xml | 28 +++
yuthon_salary/views/salary_slip_views.xml | 137 ++++++++++++
.../views/salary_structure_views.xml | 63 ++++++
22 files changed, 1605 insertions(+)
create mode 100644 yuthon_salary/__init__.py
create mode 100644 yuthon_salary/__manifest__.py
create mode 100644 yuthon_salary/data/salary_item_data.xml
create mode 100644 yuthon_salary/models/.gitkeep
create mode 100644 yuthon_salary/models/__init__.py
create mode 100644 yuthon_salary/models/class_hour.py
create mode 100644 yuthon_salary/models/class_teacher_allowance.py
create mode 100644 yuthon_salary/models/employee_salary.py
create mode 100644 yuthon_salary/models/salary_item.py
create mode 100644 yuthon_salary/models/salary_slip.py
create mode 100644 yuthon_salary/models/salary_structure.py
create mode 100644 yuthon_salary/security/ir.model.access.csv
create mode 100644 yuthon_salary/security/yuthon_salary_groups.xml
create mode 100644 yuthon_salary/security/yuthon_salary_security.xml
create mode 100644 yuthon_salary/views/class_hour_views.xml
create mode 100644 yuthon_salary/views/class_teacher_allowance_views.xml
create mode 100644 yuthon_salary/views/employee_salary_views.xml
create mode 100644 yuthon_salary/views/menu_views.xml
create mode 100644 yuthon_salary/views/salary_item_views.xml
create mode 100644 yuthon_salary/views/salary_report_views.xml
create mode 100644 yuthon_salary/views/salary_slip_views.xml
create mode 100644 yuthon_salary/views/salary_structure_views.xml
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
+
+