[ADD] 添加了 yuthon_hr_attendance 模块

This commit is contained in:
lzl
2026-09-01 22:56:39 +08:00
parent e9d4066e45
commit e3d12dc505
16 changed files with 1292 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/custom.iml" filepath="$PROJECT_DIR$/.idea/custom.iml" />
</modules>
</component>
</project>
Generated
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
</project>
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="15c9444d-3e62-40fa-bc1a-221e8a60f518" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"customColor": "",
"associatedIndex": 1
}]]></component>
<component name="ProjectId" id="3Icq6Sv8JSE7fLBO7FlFf2ztxh7" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.ShowReadmeOnStart": "true",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-410509235cf1-JavaScript-PY-242.20224.347" />
<option value="bundled-python-sdk-c2d6afa66fbe-39cff9de6eef-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-242.20224.347" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="15c9444d-3e62-40fa-bc1a-221e8a60f518" name="Changes" comment="" />
<created>1788074028307</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1788074028307</updated>
</task>
<servers />
</component>
</project>
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# email:pengyuthon@163.com
{
'name': 'Yuthon attendances',
'summary': '',
'description': '''
''',
'author': 'pengyuthon',
'version': '18.0.1.0',
'category': 'Human Resources/Attendances',
'sequence': 240,
'website': 'https://www.pengyuthon.com',
'depends': ['base', 'hr', 'hr_attendance', 'mail', 'planning', 'hr_holidays', 'resource'],
'data': [
'security/ir.model.access.csv',
'views/attendance_menu.xml',
'views/hr_attendance_views.xml',
'views/hr_attendance_month_views.xml',
'views/card_remind_form.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import hr_attendance
from . import hr_attendance_month
@@ -0,0 +1,254 @@
# -*- coding: utf-8 -*-
from datetime import datetime, time
from re import search
from odoo import models, fields, api, exceptions, _
from datetime import timedelta, date
from dateutil.relativedelta import relativedelta
from odoo.tools import format_datetime, format_duration
import logging
from odoo.exceptions import UserError, ValidationError
import calendar
_logger = logging.getLogger(__name__)
class HrAttendance(models.Model):
_inherit = "hr.attendance"
_description = "Attendance"
_rec_name = 'current_data'
is_clock_card = fields.Boolean(string="外勤")
is_no_card = fields.Boolean(string="缺卡", group_operator='count_true')
is_early_card = fields.Boolean(string="早退", group_operator='count_true')
is_late_card = fields.Boolean(string="迟到", group_operator='count_true')
is_abnormal = fields.Boolean(string="异常", group_operator='count_true')
is_expatriation = fields.Boolean(string="外派", group_operator='count_true')
user_id = fields.Many2one(related='employee_id.user_id', store=True)
absenteeism_card = fields.Boolean(string="旷工", group_operator='count_true')
work_time_type = fields.Selection([('close_day', '休息日'), ('work_day', '工作日')], string="工作时间类型")
company_id = fields.Many2one(related='employee_id.company_id', string='公司', index=True, store=True)
on_card_location = fields.Char(string="上班打卡地点")
down_card_location = fields.Char(string="下班打卡地点")
out_card_time = fields.Datetime(string="外出打卡时间")
out_card_location = fields.Char(string="外出打卡地点")
department_id = fields.Many2one(related='employee_id.department_id', string="部门", store=True)
work_hours = fields.Float(string="工作时长")
current_data = fields.Date(string='打卡日期', tracking=True)
week = fields.Selection([('one', '一'), ('two', '二'), ('three', '三'),
('four', '四'), ('five', '五'), ('six', '六'),
('seven', '日')], string='星期', compute='_compute_week', store=True)
abnormal = fields.Integer(string="异常计算")
shift_id = fields.Many2one('planning.slot', string="班次", store=True)
shift_name = fields.Char(string="班次", store=True)
is_week = fields.Boolean(string="是否为周末")
is_holiday = fields.Boolean(string="是否为节假日")
noon_duty = fields.Datetime(string="中值")
night_duty = fields.Datetime(string="晚值")
is_warning = fields.Boolean(compute='_compute_is_warning', string="警告", store=True)
@api.depends('is_no_card', 'is_early_card', 'is_late_card', 'is_abnormal', 'absenteeism_card')
def _compute_is_warning(self):
for record in self:
record.is_warning = (
record.is_no_card or
record.is_early_card or
record.is_late_card or
record.is_abnormal or
record.absenteeism_card
)
@api.depends('current_data')
def _compute_week(self):
for record in self:
if record.current_data:
weekday = (record.current_data + timedelta(hours=8)).weekday()
weekdays_cn = {0: 'one', 1: 'two', 2: 'three',
3: 'four', 4: 'five', 5: 'six',
6: 'seven'}
record.week = weekdays_cn[weekday]
else:
record.week = False
@api.depends('employee_id', 'check_in', 'check_out')
def _compute_display_name(self):
for attendance in self:
is_early_card = '早退' if attendance.is_early_card else ''
is_late_card = '迟到' if attendance.is_late_card else ''
if not attendance.check_out:
attendance.display_name = _(
str(attendance.current_data) + " 上午:%s" + is_late_card + " -下午缺卡",
format_datetime(self.env, attendance.check_in, dt_format="HH:mm"),
)
elif not attendance.check_in:
attendance.display_name = _(
str(attendance.current_data) + " 上午:缺卡-下午%s" + is_early_card,
format_datetime(self.env, attendance.check_out, dt_format="HH:mm"),
)
else:
attendance.display_name = _(
str(attendance.current_data) + " 上午:%s" + is_late_card + "-下午:%s" + is_early_card,
format_datetime(self.env, attendance.check_in, dt_format="HH:mm"),
format_datetime(self.env, attendance.check_out, dt_format="HH:mm"),
)
def update_abnormal(self):
attendance_ids = self.env["hr.attendance"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for attendance in attendance_ids:
attendance.set_shift_name()
def set_shift_name(self):
"""抽取的统一班次名称设置逻辑"""
for record in self:
# 白名单不做处理
if record.shift_id or record.employee_id.id in record.employee_id.resource_calendar_id.resource_user_ids.ids:
continue # 已有班次记录,无需自动设置
shift_days_list = []
is_shift_replacement = False
is_holiday = False
is_weekend = False
# 判断排班员工
planning_ids = self.env['planning.role'].search([('name', 'in', ['保安部', '工程部', '联安粮库'])])
related_resources = set()
for role in planning_ids:
if role.resource_ids:
related_resources.update(role.resource_ids.ids)
related_resources_set = self.env['resource.resource'].browse(related_resources)
if record.employee_id.resource_id in related_resources_set:
record.shift_name = '休息'
record.is_week = True
record.write({
'is_early_card': False,
'is_late_card': False,
'is_abnormal': False,
'is_warning': False,
'is_no_card': False,
'absenteeism_card': False,
})
continue
if record.current_data:
current_year = record.current_data.year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
leave_records = self.env['resource.calendar.leaves'].search([
("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end),
('resource_id', '=', False),
])
# 检查是否为节假日
for leave in leave_records:
if leave.date_from and leave.date_to:
from_date = fields.Date.to_date(leave.date_from + timedelta(hours=8))
to_date = fields.Date.to_date(leave.date_to + timedelta(hours=8))
if from_date <= record.current_data <= to_date:
is_holiday = True # 标记为节假日
# 收集调班补班日期
if leave.shift_replacement_day:
shift_days_list.extend(leave.shift_replacement_day.split(','))
# 检查是否为调班补班日
target_day_str = f"{record.current_data.month}月{record.current_data.day}日"
if target_day_str in shift_days_list:
is_shift_replacement = True
# 检查是否为周末
weekday = record.current_data.isoweekday()
if weekday in [6, 7]:
is_weekend = True
if is_shift_replacement:
record.shift_name = '行政班次'
elif is_weekend or is_holiday:
record.shift_name = '休息'
else:
record.shift_name = '行政班次'
if record.is_expatriation:
record.shift_name = '外派'
if record.shift_name == "休息" and not is_holiday:
record.is_week = is_weekend
else:
record.is_week = False
record.is_holiday = is_holiday
def button_all(self):
attendance_ids = self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id')))
for record in attendance_ids:
if not record.shift_id:
shift_days_list = []
is_shift_replacement = False
is_leave_day = False
is_weekend = False
if record.current_data:
# 查询休假记录
current_year = (fields.Date.today() + timedelta(hours=8)).year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
leave_records = self.env['resource.calendar.leaves'].search(
[("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end),
('resource_id', '=', False),])
# 检查是否存在匹配的休假日期
for leave in leave_records:
if leave.date_from and leave.date_to:
# 提取休假记录的月日范围
from_date = fields.Date.to_date(leave.date_from) + timedelta(days=1)
to_date = fields.Date.to_date(leave.date_to) + timedelta(days=1)
if from_date <= record.current_data <= to_date:
is_leave_day = True
if leave.shift_replacement_day:
shift_days_list.extend(leave.shift_replacement_day.split(',')) # 调班补班
target_day_str = f"{record.current_data.month}月{record.current_data.day}日"
if target_day_str in shift_days_list:
is_shift_replacement = True
weekday = record.current_data.isoweekday()
if weekday in [6, 7]:
is_weekend = True
if is_leave_day:
record.shift_name = ''
elif is_shift_replacement:
record.shift_name = '行政班次'
elif is_weekend:
record.shift_name = ''
else:
record.shift_name = '行政班次'
@api.model
def create(self, vals):
record = super(HrAttendance, self).create(vals)
# 未来日期不处理班次(保持原逻辑)
if 'current_data' in vals:
current_data = fields.Date.to_date(vals['current_data'])
today = fields.Date.today()
if current_data > today:
return record
record.set_shift_name()
return record
def write(self, vals):
result = super(HrAttendance, self).write(vals)
# 当更新考勤日期或班次时,重新计算班次名称
if 'current_data' in vals or 'shift_id' in vals:
self.set_shift_name()
return result
class card_remind_form(models.Model):
_name = 'card.remind.form'
_description = '提醒日志'
name = fields.Char(string='姓名')
start_time = fields.Datetime(string='时间')
shift = fields.Char(string='班次')
department = fields.Char(string='部门')
remarks = fields.Text(string='备注')
@@ -0,0 +1,506 @@
# -*- coding: utf-8 -*-
import datetime
import calendar
from odoo import api, fields, models, tools, _
from odoo.tools.float_utils import float_round
from dateutil.relativedelta import relativedelta
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
from datetime import timedelta
from datetime import date
class HrAttendanceYear(models.Model):
_name = "hr.attendance.year"
_description = "出勤年"
_rec_name = "year"
_order = 'year desc'
year = fields.Char(string='年份')
hr_attendance_month_ids = fields.One2many(comodel_name='hr.attendance.month', inverse_name='attendance_year_id', string="出勤月度汇总")
hr_attendance_month_detail_ids = fields.One2many(comodel_name='hr.attendance.month.detail', inverse_name='attendance_year_id', string="出勤月度汇总明细")
class HrAttendanceMonth(models.Model):
_name = "hr.attendance.month"
_description = "出勤月度汇总"
_order = "statistics_time desc"
name = fields.Char(compute='_compute_name')
attendance_year_id = fields.Many2one('hr.attendance.year', string="统计年份")
company_id = fields.Many2one('res.company', string="公司", default=lambda self: self.env.user.company_id)
current_year = fields.Char(string='统计年份', related="attendance_year_id.year")
current_month = fields.Selection([
('1', '1月份'),
('2', '2月份'),
('3', '3月份'),
('4', '4月份'),
('5', '5月份'),
('6', '6月份'),
('7', '7月份'),
('8', '8月份'),
('9', '9月份'),
('10', '10月份'),
('11', '11月份'),
('12', '12月份')
], string="考勤月份")
is_statistics = fields.Boolean(string="是否已统计", store=True, default=False)
statistics_time = fields.Datetime(string="统计时间")
attendance_month_detail_ids = fields.One2many(string="出勤月度汇总明细", comodel_name='hr.attendance.month.detail', inverse_name='hr_attendance_month_id')
is_state = fields.Boolean(string='是否已全部确认', compute='_compute_is_state', store=True)
@api.depends('attendance_month_detail_ids.state')
def _compute_is_state(self):
for record in self:
record.is_state = all(detail.state == 'confirmed' for detail in record.attendance_month_detail_ids)
def _compute_name(self):
for record in self:
record.write({'name': self.current_year+'年'+self.current_month+'月'})
def action_show_details(self):
self.ensure_one()
view = self.env.ref("yuthon_hr_attendance.view_hr_attendance_month_detail_tree")
form_view = self.env.ref("yuthon_hr_attendance.view_hr_attendance_month_detail_form")
return {
"name": _("月度汇总明细"),
"type": "ir.actions.act_window",
"view_mode": "list,form",
"res_model": "hr.attendance.month.detail",
"views": [(view.id, "list"), (form_view.id, "form")],
"view_id": view.id,
"target": "current",
"res_id": self.id,
"domain": [('hr_attendance_month_id', '=', self.id)],
"context": {'hr_attendance_month_id': self.id}
}
class HrAttendanceMonthDetail(models.Model):
_name = "hr.attendance.month.detail"
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = "考勤确认单"
name = fields.Char(related='employee_id.name')
employee_id = fields.Many2one('hr.employee', string="员工", readonly=True)
company_id = fields.Many2one(related="employee_id.company_id", string="公司", store=True)
department_id = fields.Many2one('hr.department', string="部门", related="employee_id.department_id", readonly=True, store=True)
# userid = fields.Char(string='userid')
attendance_year_id = fields.Many2one('hr.attendance.year', string="统计年份")
current_year = fields.Char(string='统计年份', related="attendance_year_id.year", store=True)
current_month = fields.Selection([
('1', '1月份'),
('2', '2月份'),
('3', '3月份'),
('4', '4月份'),
('5', '5月份'),
('6', '6月份'),
('7', '7月份'),
('8', '8月份'),
('9', '9月份'),
('10', '10月份'),
('11', '11月份'),
('12', '12月份')
], string="考勤月份", store=True)
required_attendance_days = fields.Integer(string="应出勤天数")
actual_attendance_days = fields.Integer(string="实际出勤天数")
compensatory_leave = fields.Integer(string="调休")
leave_of_absence = fields.Integer(string="事假", digits=(16, 3))
statutory_leave = fields.Integer(string="加班时(法定)")
work_overtime = fields.Integer(string="加班")
entry_date = fields.Date(related='employee_id.entry_date', string="入职日期")
expected_date = fields.Date(string="离职日期")
cop_leave = fields.Float(string='补休', digits=(16, 3))
annual_leave = fields.Float(string='年假', digits=(16, 3))
duty_off_leave = fields.Integer(string='值班调休')
marriage_leave = fields.Integer(string='婚假')
general_off_leave = fields.Integer(string='调休')
maternity_leave = fields.Integer(string='产假')
paternity_leave = fields.Integer(string='陪产假')
family_planning_leave = fields.Integer(string='计生假')
bereavement_leave = fields.Float(string='丧假')
home_visit_leave = fields.Integer(string='探亲假')
parents_meeting_leave = fields.Integer(string='家长会假')
general_sick_leave = fields.Float(string='病假(普通)')
hospital_sick_leave = fields.Float(string='病假(住院期)')
recovery_sick_leave = fields.Float(string='病假(出院恢复期)')
exam_leave = fields.Integer(string='考试假')
holiday_id = fields.Many2one('hr.leave.type', string="假期种类")
active = fields.Boolean('Active', default=True, tracking=True)
# todo 需要什么字段往上加什么
# 打卡
come_late = fields.Integer(string="迟到")
is_early_card = fields.Integer(string="早退")
is_no_card = fields.Integer(string="缺卡")
is_work = fields.Integer(string='旷工')
# 假期
unpaid_leave = fields.Integer(string="事假")
note = fields.Char(string='备注')
hr_attendance_month_id = fields.Many2one('hr.attendance.month', string="出勤月度汇总", ondelete="cascade", readonly=True)
signature = fields.Image(string="签名", copy=False, attachment=True, max_width=1024, max_height=1024)
signed_by = fields.Char(string="签名者", copy=False)
signed_on = fields.Datetime(string="签名时间", copy=False)
state = fields.Selection(
selection=[
('unconfirmed', "未确认"),
('confirmed', "已确认"),
('pending' , "待定"),
],
string="状态",
readonly=True, copy=False, index=True,
tracking=3,
default='unconfirmed')
# company_area = fields.Selection(related='department_id.company_area', string='* 所属区域')
def details_record(self):
start_datetime = datetime.datetime.strptime(f"{self.current_year}-{self.current_month}-01", "%Y-%m-%d")
start_date = start_datetime.date()
end_datetime = start_datetime + relativedelta(months=1) - relativedelta(days=1)
end_date = end_datetime.date()
domain = [
('employee_id', '=', self.employee_id.id),
('current_data', '>=', start_date),
('current_data', '<=', end_date),
]
return {
'name': '详情',
'type': 'ir.actions.act_window',
'res_model': 'hr.attendance',
'view_mode': 'list',
'domain': domain,
}
def attendance_confirm(self):
detail_ids = self.env["hr.attendance.month.detail"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for record in detail_ids:
record.state = 'confirmed'
def button_pending(self):
for record in self:
record.write({'state': 'pending'})
def _get_required_attendance_days(self, employee):
"""
应出勤天数(按员工考勤日历的工作日统计)
:param employee: 员工
:return: 应出勤天数
"""
self.ensure_one()
days = calendar.monthrange(int(self.current_year), int(self.current_month))[1]
calendar_attendance = employee.resource_calendar_id.attendance_ids
work_days = {str(a.dayofweek) for a in calendar_attendance}
required_attendance_days = 0
for n in range(days):
current_date = datetime.date(int(self.current_year), int(self.current_month), n + 1)
# 0=周一 ... 6=周日
if str(current_date.weekday()) in work_days:
required_attendance_days += 1
return required_attendance_days
def _get_actual_attendance_days(self, employee):
"""
实际出勤天数
:param employee: 员工
:return: 实际出勤天数
"""
self.ensure_one()
days = calendar.monthrange(int(self.current_year), int(self.current_month))[1]
actual_attendance_days = 0
for n in range(days):
# 根据出勤时间判断是否打卡
current_date = datetime.date(int(self.current_year), int(self.current_month), n + 1)
attendance_obj = self.env["hr.attendance"].sudo().search([('employee_id', '=', employee.id), ("current_data", '=', current_date)])
is_check_in = False
for obj in attendance_obj:
if obj.check_in or obj.check_out:
# 当天有打卡记录说明已经出勤
is_check_in = True
if is_check_in is True:
actual_attendance_days += 1
return actual_attendance_days
def _get_come_late(self, employee):
"""
判断是否迟到
:param employee: 员工
:return: 迟到次数
"""
self.ensure_one()
days = calendar.monthrange(int(self.current_year), int(self.current_month))[1]
is_late_card = 0
is_early_card = 0
is_no_card = 0
for n in range(days):
# 根据出勤时间判断是否打卡
current_date = datetime.date(int(self.current_year), int(self.current_month), n + 1)
attendance_obj = self.env["hr.attendance"].sudo().search([('employee_id', '=', employee.id), ("current_data", '=', current_date)])
for obj in attendance_obj:
# 迟到
if obj.is_late_card:
is_late_card += 1
# 早退
if obj.is_early_card:
is_early_card += 1
# 缺卡
if obj.is_no_card:
is_no_card += 1
return is_late_card, is_early_card, is_no_card
def _hr_attendance_month_detail_action(self, processed_employee_ids):
"""
返回视图 view_hr_attendance_month_detail_tree
:return: ir.actions.act_window 视图 view_hr_attendance_month_detail_tree
"""
action = {
'type': 'ir.actions.act_window',
'res_model': 'hr.attendance.month.detail',
'name': "考勤统计",
'view_type': 'list',
'view_mode': 'list,form',
'domain': [('employee_id', 'in', processed_employee_ids)] if processed_employee_ids else [],
'views': [[self.env.ref('yuthon_hr_attendance.view_hr_attendance_month_detail_tree').id, 'list'],
[False, 'form']],
}
return action
def _get_hr_leave_type(self, employee_id, type_list):
"""
:param type_list: 请假类型列表
:return: 天
"""
self.ensure_one()
leave_days = {}
year = int(self.current_year)
month = int(self.current_month)
if month == 12:
date_to = date(year, 12, 31)
else:
next_month = month + 1
date_to = date(year, next_month, 1) - datetime.timedelta(days=1)
date_from = date(year, month, 1)
for leave_type in type_list:
holiday_status_id = self.env['hr.leave.type'].search([('name', '=', leave_type)])
leave_records = self.env['hr.leave'].search([
('employee_id', '=', employee_id.id),
('holiday_status_id', '=', holiday_status_id.id),
('date_from', '<', date_to),
('date_to', '>', date_from),
('state', '=', 'validate')
])
leave_days[leave_type] = 0
for leave in leave_records:
leave_start = leave.date_from.date()
leave_end = leave.date_to.date()
start_in_month = max(leave_start, date_from)
end_in_month = min(leave_end, date_to)
# 判断是否跨月
is_cross_month = leave_start.year != leave_end.year or leave_start.month != leave_end.month
if is_cross_month:
if leave_start.year == year and leave_start.month == month:
start_date = leave_start
if leave_end.year == year and leave_end.month == month:
end_date = leave_end
else:
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
end_date = end_date - datetime.timedelta(days=1) # 取到当月最后一天
elif leave_end.year == year and leave_end.month == month:
start_date = date(year, month, 1)
end_date = leave_end
else:
start_date = start_in_month
end_date = end_in_month
days = self._compute_duration(holiday_status_id, start_date, end_date)
else:
days = self._compute_duration(holiday_status_id, start_in_month, end_in_month)
if leave_type in leave_days:
leave_days[leave_type] += days
else:
leave_days[leave_type] = days
if leave_type not in leave_days:
leave_days[leave_type] = 0
return leave_days
def _compute_duration(self, holiday_status_id, start_date, end_date):
"""
计算请假时长
:param holiday_status_id: 请假类型
:param start_date: 开始时间
:param end_date: 结束时间
:return:天
"""
# 如果是自然日:
if holiday_status_id.type_of_work == "1":
if start_date and end_date:
days = (end_date - start_date).days + 1
else:
days = 0
return days
if holiday_status_id.type_of_work == 0:
current_year = (fields.Date.today() + timedelta(hours=8)).year
query_start = datetime.datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime.datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
calendar_leaves = self.env['resource.calendar.leaves'].sudo().search([
("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end),
('resource_id', '=', False),
])
holidays_list = []
shift_days_list = []
# 提取节假日和调休日
for leave in calendar_leaves:
if leave.holidays:
holidays_list.extend(leave.holidays.split(','))
if leave.alternate_rest_day:
holidays_list.extend(leave.alternate_rest_day.split(','))
if leave.shift_replacement_day:
shift_days_list.extend(leave.shift_replacement_day.split(','))
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
days = 0 # 初始化工作日计数
current_date = start_date
while current_date <= end_date:
current_date_str = current_date.strftime('%m月%d日')
current_date_str = self.remove_leading_zero(current_date_str)
is_weekend = current_date.weekday() in [5, 6]
if current_date_str in holidays_list:
# 如果是法定节假日,不计为工作日
pass
elif current_date_str in shift_days_list:
# 如果是调休日,无论是否周末都算作工作日
days += 1
elif not is_weekend:
# 如果不是周末且不是节假日,算作工作日
days += 1
current_date += timedelta(days=1)
else:
delta = end_date - start_date
days = delta.days + 1
return days
def remove_leading_zero(self, date_str):
month, day = date_str.split('月')
month = month.lstrip('0')
day = day.rstrip('日')
day = day.lstrip('0')
return f'{month}月{day}日'
def analysis_attendance_month_detail(self):
"""
更新用户的考勤明细
:return:
"""
self.ensure_one()
if self.state == 'confirmed' and self.signature:
return False
# 排班的进行统计
if self.employee_id.default_planning_role_id:
# todo 需要什么字段往上加什么
# 迟到, 早退, 缺卡
self.come_late, self.is_early_card, self.is_no_card = self._get_come_late(self.employee_id)
# 无薪假:hr.leave表
days, = self._get_hr_leave_type(self.employee_id, ['事假', '年假'])
self.unpaid_leave = days
# 行政的进行统计
if not self.employee_id.default_planning_role_id:
# todo 需要什么字段往上加什么
# 迟到, 早退, 缺卡
self.come_late, self.is_early_card, self.is_no_card = self._get_come_late(self.employee_id)
# 无薪假:hr.leave表
leave_days = self._get_hr_leave_type(self.employee_id,['事假', '年假', '产假'])
self.leave_of_absence = leave_days.get('事假', 0)
self.annual_leave = leave_days.get('年假', 0)
self.maternity_leave = leave_days.get('产假', 0)
return True
def update_attendance_month(self):
"""
调用用户的考勤明细
:return:
"""
for record in self:
# 更新用户的考勤明细
record.analysis_attendance_month_detail()
def _get_attendance_month_obj(self):
"""
获取出勤月度汇总记录结果表
:return: obj
"""
attendance_month_obj = self.env["hr.attendance.month"].search([
('current_year', '=', self.current_year),
('current_month', '=', self.current_month)
], limit=1)
if not attendance_month_obj:
attendance_month_obj = self.env["hr.attendance.month"].sudo().create({
"attendance_year_id": self.attendance_year_id.id,
"current_year": self.current_year,
"current_month": self.current_month,
"is_statistics": True,
"statistics_time": fields.Datetime.now(),
})
return attendance_month_obj
def _get_attendance_month_detail(self, employee, current_year, current_month):
"""
判断当前是否已经创建了考勤明细
:param employee: 员工
:return: 考情明细
"""
attendance_month_detail_obj = self.env["hr.attendance.month.detail"].search(
[('employee_id', '=', employee.id),
('current_year', '=', current_year),
('current_month', '=', current_month),
], limit=1)
if not attendance_month_detail_obj:
attendance_month_detail_obj = self.env["hr.attendance.month.detail"].sudo().create({
"employee_id": employee.id,
"attendance_year_id": self.attendance_year_id.id,
"current_year": current_year,
"current_month": current_month,
})
return attendance_month_detail_obj
def button_compute_all(self):
processed_employee_ids = []
attendance_month_obj = self._get_attendance_month_obj()
# 查询所有的当前员工? 是否需要判断当前员工是否关联企业微信??预留
calendars = self.env['resource.calendar'].search([])
all_employee_ids = set()
for calendar in calendars:
all_employee_ids.update(calendar.resource_user_ids.ids)
all_employee_ids = list(all_employee_ids)
detail_ids = self.env["hr.attendance.month.detail"].browse(self._context.get('active_ids', self._context.get('active_id')))
for detail_id in detail_ids:
if detail_id.employee_id not in all_employee_ids:
# 判断当前是否已经创建了考勤明细
attendance_month_detail_obj = self._get_attendance_month_detail(detail_id.employee_id, detail_id.current_year, detail_id.current_month)
attendance_month_detail_obj.hr_attendance_month_id = attendance_month_obj.id
# 判断当前用户是否已经确认签字,签字后不再更新
if attendance_month_detail_obj.state == 'confirmed' and attendance_month_detail_obj.signature:
continue
# 更新用户的考勤明细
attendance_month_detail_obj.analysis_attendance_month_detail()
if detail_id.employee_id.id not in processed_employee_ids:
processed_employee_ids.append(detail_id.employee_id.id)
# 返回包含处理过的员工ID的action动作
action = self._hr_attendance_month_detail_action(processed_employee_ids)
return action
def attendance_month_detail(self):
attendance_details = self.env['hr.attendance.month.detail'].search([])
for detail in attendance_details:
detail.write({'state': 'confirmed'})
@@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_attendance_year_manager,hr.attendance.year.manager,model_hr_attendance_year,base.group_user,1,1,1,1
access_hr_attendance_month_manager,hr.attendance.month.manager,model_hr_attendance_month,base.group_user,1,1,0,1
access_hr_attendance_month_detail_user,hr.attendance.month.detail.user,model_hr_attendance_month_detail,base.group_user,1,0,0,0
access_hr_attendance_month_detail_manager,hr.attendance.month.detail.manager,model_hr_attendance_month_detail,base.group_user,1,1,1,1
access_card_remind_form,card.remind.form,model_card_remind_form,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_attendance_year_manager hr.attendance.year.manager model_hr_attendance_year base.group_user 1 1 1 1
3 access_hr_attendance_month_manager hr.attendance.month.manager model_hr_attendance_month base.group_user 1 1 0 1
4 access_hr_attendance_month_detail_user hr.attendance.month.detail.user model_hr_attendance_month_detail base.group_user 1 0 0 0
5 access_hr_attendance_month_detail_manager hr.attendance.month.detail.manager model_hr_attendance_month_detail base.group_user 1 1 1 1
6 access_card_remind_form card.remind.form model_card_remind_form base.group_user 1 1 1 1
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="hr_attendance_root" name="考勤管理" sequence="30"/>
<menuitem id="menu_hr_attendance_record" name="打卡记录" parent="hr_attendance_root" sequence="10" action="action_attendance_company_form"/>
<menuitem id="menu_hr_attendance_my" name="我的考勤" parent="hr_attendance_root" sequence="20" action="action_hr_attendance_my"/>
<menuitem id="menu_hr_attendance_my_abnormal" name="我的异常" parent="hr_attendance_root" sequence="30" action="action_hr_attendance_my_abnormal"/>
<menuitem id="menu_hr_attendance_month_board" name="月度考勤" parent="hr_attendance_root" sequence="40"/>
<menuitem id="menu_hr_attendance_month_view" name="出勤统计" parent="menu_hr_attendance_month_board" sequence="10" action="hr_attendance_month_action"/>
<menuitem id="menu_hr_attendance_year_view" name="出勤年份维护" parent="menu_hr_attendance_month_board" sequence="20" action="hr_attendance_year_action"/>
<menuitem id="menu_hr_attendance_month_detail_view" name="考勤确认单" parent="menu_hr_attendance_month_board" sequence="30" action="hr_attendance_month_detail_action"/>
<menuitem id="menu_hr_attendance_ce_record" name="考勤提醒表单" parent="hr_attendance_root" sequence="50" action="view_action_card_remind"/>
</odoo>
@@ -0,0 +1,30 @@
<odoo>
<record id="view_tree_card_remind" model="ir.ui.view">
<field name="name">card.remind.tree</field>
<field name="model">card.remind.form</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="name"/>
<field name="start_time"/>
<field name="shift"/>
<field name="department"/>
<field name="remarks"/>
</list>
</field>
</record>
<record id="view_tree_card_search" model="ir.ui.view">
<field name="name">card.remind.search</field>
<field name="model">card.remind.form</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
</search>
</field>
</record>
<record id="view_action_card_remind" model="ir.actions.act_window">
<field name="name">考勤提醒表单</field>
<field name="res_model">card.remind.form</field>
<field name="view_mode">list</field>
<field name="context">{'group_by': ['department', 'name']}</field>
</record>
</odoo>
@@ -0,0 +1,278 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_hr_attendance_year_tree" model="ir.ui.view">
<field name="name">hr.attendance.year.tree</field>
<field name="model">hr.attendance.year</field>
<field name="arch" type="xml">
<list string="hr attendance year" editable="top">
<field name="year"/>
</list>
</field>
</record>
<record id="hr_attendance_year_action" model="ir.actions.act_window">
<field name="name">出勤年份维护</field>
<field name="res_model">hr.attendance.year</field>
<field name="view_mode">list,form</field>
</record>
<record id="view_hr_attendance_month_tree" model="ir.ui.view">
<field name="name">hr.attendance.month.tree</field>
<field name="model">hr.attendance.month</field>
<field name="arch" type="xml">
<list string="hr attendance month">
<field name="current_year"/>
<field name="current_month"/>
<field name="is_statistics"/>
<field name="statistics_time"/>
<field name="company_id" column_invisible="1"/>
<field name="is_state" column_invisible="1"/>
<button name="action_show_details" string="汇总明细" type="object" title="Show Details" icon="fa-list"/>
</list>
</field>
</record>
<record id="view_hr_attendance_month_form" model="ir.ui.view">
<field name="name">hr.attendance.month.form</field>
<field name="model">hr.attendance.month</field>
<field name="arch" type="xml">
<form string="hr attendance month">
<sheet>
<group>
<field name="current_year"/>
<field name="current_month"/>
<field name="is_statistics"/>
<field name="statistics_time"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_attendance_month_action" model="ir.actions.act_window">
<field name="name">出勤统计</field>
<field name="res_model">hr.attendance.month</field>
<field name="view_mode">list,form</field>
</record>
<record id="view_hr_attendance_month_detail_tree" model="ir.ui.view">
<field name="name">hr.attendance.month.detail.tree</field>
<field name="model">hr.attendance.month.detail</field>
<field name="arch" type="xml">
<list string="hr attendance detail" editable="bottom"
decoration-success="state == 'confirmed'"
decoration-danger="state == 'unconfirmed'">
<header>
<button class="oe_stat_button" name="attendance_confirm" string="考勤确认" type="object"
icon="fa-bars"/>
<button class="oe_stat_button" name="update_attendance_month" string="更新考勤明细" type="object"
icon="fa-bars"/>
</header>
<field name="employee_id"/>
<field name="department_id" optional="show"/>
<field name="current_year"/>
<field name="current_month"/>
<field name="entry_date"/>
<field name="expected_date"/>
<field name="work_overtime"/>
<!-- todo 需要什么字段往上加什么-->
<!-- 打卡-->
<field name="come_late"/>
<field name="is_early_card"/>
<field name="is_no_card"/>
<field name="is_work"/>
<!-- 请假-->
<field name="unpaid_leave"/>
<field name="cop_leave"/>
<field name="annual_leave"/>
<field name="duty_off_leave"/>
<field name="marriage_leave"/>
<field name="general_off_leave"/>
<field name="maternity_leave"/>
<field name="paternity_leave"/>
<field name="family_planning_leave"/>
<field name="bereavement_leave"/>
<field name="home_visit_leave"/>
<field name="parents_meeting_leave"/>
<field name="general_sick_leave"/>
<field name="hospital_sick_leave"/>
<field name="recovery_sick_leave"/>
<field name="exam_leave"/>
<!-- 其他-->
<field name="holiday_id"/>
<field name="note"/>
<field name="state" widget="badge" decoration-info="state == 'unconfirmed'"
decoration-success="state == 'confirmed'"/>
<field name="company_id" optional="show"/>
<button name="details_record" string="详情" type="object" class="oe_highlight"/>
</list>
</field>
</record>
<record id="view_hr_attendance_month_detail_tree1" model="ir.ui.view">
<field name="name">hr.attendance.month.detail.tree</field>
<field name="model">hr.attendance.month.detail</field>
<field name="arch" type="xml">
<list string="hr attendance detail" decoration-danger="state != 'confirmed'">
<field name="current_year"/>
<field name="current_month"/>
<field name="entry_date"/>
<field name="expected_date"/>
<field name="work_overtime"/>
<!-- todo 需要什么字段往上加什么-->
<!-- 打卡-->
<field name="come_late"/>
<field name="is_early_card"/>
<field name="is_no_card"/>
<field name="is_work"/>
<!-- 请假-->
<field name="unpaid_leave"/>
<field name="cop_leave"/>
<field name="annual_leave"/>
<field name="duty_off_leave"/>
<field name="marriage_leave"/>
<field name="general_off_leave"/>
<field name="maternity_leave"/>
<field name="family_planning_leave"/>
<field name="bereavement_leave"/>
<field name="home_visit_leave"/>
<field name="parents_meeting_leave"/>
<field name="general_sick_leave"/>
<field name="hospital_sick_leave"/>
<field name="recovery_sick_leave"/>
<field name="exam_leave"/>
<!-- 其他-->
<field name="holiday_id" invisible="1"/>
<field name="note"/>
<field name="state" widget="badge" decoration-info="state == 'unconfirmed'"
decoration-success="state == 'confirmed'"/>
<button name="button_pending" string="待定" type="object" class="oe_highlight"/>
</list>
</field>
</record>
<record id="view_hr_attendance_month_detail_form1" model="ir.ui.view">
<field name="name">hr.attendance.month.detail.form</field>
<field name="model">hr.attendance.month.detail</field>
<field name="arch" type="xml">
<form create="0" delete="0" edit="0">
<sheet>
<group>
<group>
<field name="current_year"/>
<field name="current_month"/>
<field name="entry_date"/>
<field name="expected_date"/>
<field name="work_overtime"/>
</group>
<group>
<field name="come_late"/>
<field name="is_early_card"/>
<field name="is_no_card"/>
<field name="is_work"/>
</group>
</group>
<group>
<group>
<field name="unpaid_leave"/>
<field name="cop_leave"/>
<field name="annual_leave"/>
<field name="duty_off_leave"/>
<field name="marriage_leave"/>
<field name="general_off_leave"/>
<field name="maternity_leave"/>
<field name="family_planning_leave"/>
<field name="bereavement_leave"/>
<field name="home_visit_leave"/>
<field name="parents_meeting_leave"/>
<field name="general_sick_leave"/>
<field name="hospital_sick_leave"/>
<field name="recovery_sick_leave"/>
<field name="exam_leave"/>
</group>
<group>
<field name="holiday_id" invisible="1"/>
<field name="note"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_hr_attendance_month_detail_form" model="ir.ui.view">
<field name="name">hr.attendance.month.detail.form</field>
<field name="model">hr.attendance.month.detail</field>
<field name="arch" type="xml">
<form string="hr attendance month detail">
<sheet>
<group>
<group>
<field name="employee_id"/>
<field name="department_id"/>
<field name="current_year"/>
<field name="current_month"/>
<field name="required_attendance_days"/>
<field name="actual_attendance_days"/>
<field name="holiday_id"/>
<!-- <field name="company_area"/>-->
</group>
<group>
<field name="compensatory_leave"/>
<field name="leave_of_absence"/>
<field name="come_late"/>
<field name="work_overtime"/>
<field name="statutory_leave" string="加班时(法定假)"/>
<field name="note"/>
<field name="state"/>
</group>
</group>
<notebook>
<page string="客户签名" name="customer_signature">
<group>
<field name="signed_by"/>
<field name="signed_on"/>
<field name="signature" widget="image"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_hr_attendance_month_detail_search" model="ir.ui.view">
<field name="name">hr.attendance.month.detail.search</field>
<field name="model">hr.attendance.month.detail</field>
<field name="arch" type="xml">
<search>
<field name="employee_id"/>
<field name="department_id"/>
<field name="attendance_year_id"/>
<field name="current_month"/>
<group expand="0" string="Group By">
<filter name="group_by_current_year_id" string="年份" context="{'group_by':'attendance_year_id'}"/>
<filter name="group_by_current_month" string="月份" context="{'group_by':'current_month'}"/>
</group>
</search>
</field>
</record>
<record id="hr_attendance_month_detail_action" model="ir.actions.act_window">
<field name="name">考勤确认单</field>
<field name="res_model">hr.attendance.month.detail</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_hr_attendance_month_detail_search"/>
<field name="context">
{
"search_default_group_by_current_year_id":1,
"search_default_group_by_current_month":1
}
</field>
</record>
<record id="hr_attendance_month_my_detail_action" model="ir.actions.act_window">
<field name="name">考勤确认</field>
<field name="res_model">hr.attendance.month.detail</field>
<field name="view_mode">list,form</field>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('yuthon_hr_attendance.view_hr_attendance_month_detail_tree1')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('yuthon_hr_attendance.view_hr_attendance_month_detail_form1')})]"/>
</record>
</odoo>
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_attendance_going_tree3" model="ir.ui.view">
<field name="name">hr.attendance.going.list</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<list string="出勤"
editable="bottom" default_order="current_data desc">
<field name="employee_id" options="{'no_create': True}"/>
<field name="is_expatriation" column_invisible="1"/>
<field name="current_data" optional="show"/>
<field name="week" optional="show"/>
<field name="check_in" optional="show" widget="time"/>
<field name="check_out" optional="show" widget="time"/>
<field name="shift_id" column_invisible="1"/>
<field name="shift_name" optional="show"/>
<field name="is_week" column_invisible="1"/>
<field name="is_holiday" column_invisible="1"/>
<field name="create_date"/>
<field name="user_id" column_invisible="1"/>
<field name="is_clock_card" column_invisible="1"/>
<field name="department_id" column_invisible="1"/>
<field name="is_no_card" optional="show"/>
<field name="is_early_card" optional="show"/>
<field name="is_late_card" optional="show"/>
<field name="absenteeism_card" optional="show" string="旷工"/>
<field name="is_abnormal" optional="show" string="异常"/>
<field name="abnormal" column_invisible="1"/>
<field name="work_time_type" column_invisible="1"/>
<field name="is_warning" column_invisible="1"/>
</list>
</field>
</record>
<record id="view_attendance_going_search" model="ir.ui.view">
<field name="name">hr.attendance.search</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<search>
<group expand="0" name="group_by" string="Group By">
<filter string="打卡日期" name="group_by_current_data" domain="[]"
context="{'group_by' : 'current_data'}" />
</group>
</search>
</field>
</record>
<record id="view_hr_attendance_search" model="ir.ui.view">
<field name="name">hr.attendance.search</field>
<field name="model">hr.attendance</field>
<field name="arch" type="xml">
<search>
<field name="employee_id" />
<group expand="0" name="group_by" string="Group By">
<filter string="打卡日期" name="group_by_current_data" domain="[]"
context="{'group_by' : 'current_data'}" />
<filter string="公司" name="group_by_company_id" domain="[]"
context="{'group_by' : 'company_id'}" />
<filter string="部门" name="group_by_department_id" domain="[]"
context="{'group_by' : 'department_id'}" />
<filter string="员工" name="group_by_employee_id" domain="[]"
context="{'group_by' : 'employee_id'}" />
</group>
</search>
</field>
</record>
<record id="action_hr_attendance_my" model="ir.actions.act_window">
<field name="name">我的考勤</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">list</field>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
<field name="search_view_id" ref="view_attendance_going_search" />
<field name="context">{'search_default_group_by_current_data': 1}</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('yuthon_hr_attendance.view_attendance_going_tree3')})]" />
</record>
<record id="action_hr_attendance_my_abnormal" model="ir.actions.act_window">
<field name="name">我的异常</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">list,form</field>
<field name="domain">[('employee_id.user_id', '=', uid), '|', '|', '|', '|', ('is_abnormal',
'=', True), ('is_early_card', '=', True), ('is_late_card', '=', True),
('absenteeism_card', '=', True), ('is_no_card', '=', True)]</field>
<field name="search_view_id" ref="view_attendance_going_search" />
<field name="context">{'search_default_group_by_current_data': 1}</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('yuthon_hr_attendance.view_attendance_going_tree3')})]" />
</record>
<record id="action_attendance_company_form" model="ir.actions.act_window">
<field name="name">考勤记录</field>
<field name="res_model">hr.attendance</field>
<field name="view_mode">list</field>
<field name="search_view_id" ref="view_hr_attendance_search" />
</record>
</odoo>