107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
from odoo import fields, models, api
|
|
|
|
# 教学用扣款规则(可在 production 改为可配置的 salary.rule 模型)
|
|
LATE_PENALTY = 50.0 # 迟到一次扣 50
|
|
DAYS_PER_MONTH = 21.75 # 月计薪天数,用于把“缺勤天数”折算成金额
|
|
|
|
|
|
class Payslip(models.Model):
|
|
"""工资条:聚合员工当月出勤,自动计算扣款与实发。"""
|
|
_name = 'yuthon.payslip'
|
|
_description = '工资条'
|
|
_order = 'period_end desc, employee_id'
|
|
|
|
employee_id = fields.Many2one(
|
|
'yuthon.employee',
|
|
string='教职工',
|
|
required=True,
|
|
ondelete='cascade',
|
|
)
|
|
period_start = fields.Date(string='起始日期', required=True)
|
|
period_end = fields.Date(string='结束日期', required=True)
|
|
|
|
# ---- 金额 ----
|
|
basic_wage = fields.Float(
|
|
string='基本工资',
|
|
related='employee_id.base_salary',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
allowance = fields.Float(string='津贴/补贴', default=0.0)
|
|
|
|
# ---- 出勤聚合(自动统计)----
|
|
late_count = fields.Integer(
|
|
string='迟到次数', compute='_compute_attendance_stats', store=True)
|
|
absent_days = fields.Integer(
|
|
string='缺勤天数', compute='_compute_attendance_stats', store=True)
|
|
|
|
# ---- 扣款(自动计算)----
|
|
late_deduction = fields.Float(
|
|
string='迟到扣款', compute='_compute_deductions', store=True)
|
|
absent_deduction = fields.Float(
|
|
string='缺勤扣款', compute='_compute_deductions', store=True)
|
|
attendance_deduction = fields.Float(
|
|
string='出勤扣款合计', compute='_compute_deductions', store=True)
|
|
|
|
# ---- 实发 ----
|
|
net_salary = fields.Float(
|
|
string='实发工资', compute='_compute_net', store=True)
|
|
|
|
state = fields.Selection(
|
|
[('draft', '草稿'), ('done', '已确认')],
|
|
string='状态',
|
|
default='draft',
|
|
)
|
|
|
|
# ---- 计算逻辑 ----
|
|
@api.depends('employee_id', 'period_start', 'period_end')
|
|
def _compute_attendance_stats(self):
|
|
Attendance = self.env['yuthon.attendance']
|
|
for rec in self:
|
|
domain = [('employee_id', '=', rec.employee_id.id)]
|
|
if rec.period_start:
|
|
domain.append(('date', '>=', rec.period_start))
|
|
if rec.period_end:
|
|
domain.append(('date', '<=', rec.period_end))
|
|
# sudo() 确保在后台重算时不受当前用户 ACL 影响
|
|
rec.late_count = Attendance.sudo().search_count(
|
|
domain + [('status', '=', 'late')])
|
|
rec.absent_days = Attendance.sudo().search_count(
|
|
domain + [('status', '=', 'absent')])
|
|
|
|
@api.depends('late_count', 'absent_days', 'basic_wage')
|
|
def _compute_deductions(self):
|
|
for rec in self:
|
|
rec.late_deduction = rec.late_count * LATE_PENALTY
|
|
daily = (rec.basic_wage / DAYS_PER_MONTH) if rec.basic_wage else 0.0
|
|
rec.absent_deduction = rec.absent_days * daily
|
|
rec.attendance_deduction = rec.late_deduction + rec.absent_deduction
|
|
|
|
@api.depends('basic_wage', 'allowance', 'attendance_deduction')
|
|
def _compute_net(self):
|
|
for rec in self:
|
|
rec.net_salary = rec.basic_wage + rec.allowance - rec.attendance_deduction
|
|
|
|
# ---- 业务动作 ----
|
|
def action_confirm(self):
|
|
self.write({'state': 'done'})
|
|
|
|
def action_draft(self):
|
|
self.write({'state': 'draft'})
|
|
|
|
|
|
class Employee(models.Model):
|
|
"""_inherit 反向扩展:把工资条挂到员工上。"""
|
|
_inherit = 'yuthon.employee'
|
|
|
|
payslip_ids = fields.One2many(
|
|
'yuthon.payslip', 'employee_id', string='工资条')
|
|
payslip_count = fields.Integer(
|
|
string='工资条数', compute='_compute_payslip_count')
|
|
|
|
@api.depends('payslip_ids')
|
|
def _compute_payslip_count(self):
|
|
for rec in self:
|
|
rec.payslip_count = len(rec.payslip_ids)
|