diff --git a/planning_gantt/__init__.py b/planning_gantt/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/planning_gantt/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/planning_gantt/__manifest__.py b/planning_gantt/__manifest__.py new file mode 100644 index 000000000..88629423c --- /dev/null +++ b/planning_gantt/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Planning Gantt", + 'summary': "Planning Gantt", + 'description': 'Planning Gantt', + 'author': 'XSK', + 'website': '', + 'website': '', + 'category': 'Tools', + 'version': '17.0', + 'depends': ['base', 'planning'], + 'data': [ + 'views/plan_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'planning_gantt/static/src/**/*', + ], + }, +} + diff --git a/planning_gantt/static/src/js/planning_gantt_renderer.js b/planning_gantt/static/src/js/planning_gantt_renderer.js new file mode 100644 index 000000000..4296afd33 --- /dev/null +++ b/planning_gantt/static/src/js/planning_gantt_renderer.js @@ -0,0 +1,214 @@ +/** @odoo-module **/ +import { patch } from "@web/core/utils/patch"; +import { PlanningGanttRenderer } from "@planning/views/planning_gantt/planning_gantt_renderer"; + + +PlanningGanttRenderer.headerTemplate = "planning.PlanningGanttRenderer.Header"; +// 修改 PlanningGanttRenderer 的 totalRowTemplate 属性为自定义模板 +PlanningGanttRenderer.totalRowTemplate = "planning.PlanningGanttRenderer.TotalRow"; + +function floatToTime(floatTime) { + // 提取小时部分 + const hours = Math.floor(floatTime); + // 提取分钟部分 + const minutes = Math.round((floatTime - hours) * 60); + + // 格式化小时和分钟为两位数 + const formattedHours = String(hours).padStart(2, '0'); + const formattedMinutes = String(minutes).padStart(2, '0'); + + // 返回格式化后的时间字符串 + return `${formattedHours}:${formattedMinutes}`; +} + +// 对 PlanningGanttRenderer 原型进行补丁修改 +patch(PlanningGanttRenderer.prototype, { + computeDerivedParams() { + super.computeDerivedParams(); + const { displayTotalRow } = this.model.metaData; + + if (displayTotalRow) { + const prePills = this.getPills(); + this.workShiftRow = this.getTotalRowGroupByWorkShift(prePills); + } + }, + + getPillGroup(pill) { + return pill.record.template_id || undefined; + }, + + /** + * 将pills按照班次分组 + */ + getPillsGroupByWorkShift(pills) { + const groupPills = []; + // 遍历pills,根据pills.record将pills进行白班,值班,中夜分组 + pills.forEach(pill => { + const groupBy = this.getPillGroup(pill); + if (!groupBy) { + groupPills.push({id: 0, name: "未知班次", pill: pill}) + } else { + groupPills.push({id: parseInt(groupBy[0], 10), name: groupBy[1], pill: pill}) + } + }); + // 遍历groupPills,根据type进行分组,并计算数量 + const groupDict = {}; + groupPills.forEach(groupPill => { + const pill = groupPill.pill; + const key = groupPill.id; + if (!groupDict[key]) { + groupDict[key] = { + id: groupPill.id, + name: groupPill.name, + pills: [pill], + count: 1, + isGroup: true, + _color: pill._color, + }; + } else { + groupDict[key].pills.push(pill); + groupDict[key].count++; + } + }); + + return groupDict; + }, + + addToWorkShift(pill, group) { + group.pills.push(pill); + group.aggregateValue += pill.allocatedHours[group.col]; + return true; + }, + + // 聚合数据 + aggregateWorkShiftPills(pills) { + /** @type {Record} */ + const groups = {}; + + // 初始化每列的分组 + for (let col = 1; col <= this.subColumns.length; col++) { + groups[col] = { + break: false, // 是否需要与前一个分组分开,true 表示无法合并 + col, // 当前列编号 + pills: [], // 当前分组中的 pills 数组 + aggregateValue: 0, // 分组的汇总值 + grid: { column: [col, 1] }, // 分组的网格位置(列信息) + }; + } + + // 遍历每一个 pill,决定其放置的分组 + for (const pill of pills) { + const firstCol = this.getFirstcol(pill); // 获取 pill 的第一列 + const group = groups[firstCol]; // 获取第一列对应的分组 + + // 只聚合第一列的 pill + const added = this.addToWorkShift(pill, group); // 尝试将 pill 添加到分组 + if (added) { + group.aggregateValue += pill.value || 0; // 只计算第一列的聚合值 + group.break = true; // 标记为不可合并 + } + } + + // 过滤掉没有 pill 的空分组 + const filteredGroups = Object.values(groups).filter((g) => g.pills.length); + + // 如果满足条件,则合并分组 + if (this.shouldMergeGroups()) { + return this.mergeGroups(filteredGroups); + } + + return filteredGroups; // 返回最终分组 + }, + + // 获取班次模板信息 + getSlotTemplate(ids) { + const templateIds = ids.map(id => parseInt(id, 10)).filter(id => id !== 0); + if (!templateIds.length) { + return {}; + } + let slotTemplateData; + const jsonData = { + id: Math.floor(Math.random() * 1000 * 1000 * 1000), + jsonrpc: "2.0", + method: "call", + params: { + args: [], + method: "web_search_read", + model: "planning.slot.template", + kwargs: { + context: {}, + count_limit: 10001, + domain: [['id', 'in', templateIds]], + limit: 10001, + offset: 0, + specification: { + display_name: {}, + start_time: {}, + end_time: {}, + }, + }, + } + }; + $.ajax({ + url: "/web/dataset/call_kw/planning.slot.template/web_search_read", + method: "POST", + dataType: 'json', + async: false, // 设置为同步请求 + beforeSend: (xhr) => {xhr.setRequestHeader('Content-Type', 'application/json');}, + data : JSON.stringify(jsonData), + success: (result) => { + slotTemplateData = result?.result?.records || []; + }, + error: function(xhr, status, error) { + // 请求失败的处理逻辑 + console.error("Error:", error); + } + }); + const dataDict = {}; + slotTemplateData.forEach(item => { + dataDict[item.id] = item; + }); + return dataDict; + }, + + // 填充模板信息 + fetchSlotTemplate(groupDict) { + // 添加模板信息数据 + const slotTemplateData = this.getSlotTemplate(Object.keys(groupDict)); + // 填充groupDict信息 + Object.keys(groupDict).forEach(key => { + const group = groupDict[key]; + const groupSlotTemplate = slotTemplateData[group.id]; + group.start_time = groupSlotTemplate?.start_time || 0; + group.end_time = groupSlotTemplate?.end_time || 0; + // 拼接显示信息 + group.ganttLegend = `${groupSlotTemplate.display_name} : ${floatToTime(group.start_time)} - ${floatToTime(group.end_time)}`; + }); + }, + + /** + * 获取排班班次分组总计 + * @param pills + */ + getTotalRowGroupByWorkShift(pills) { + // 将数据按照排班班次分组 + const groupPills = this.getPillsGroupByWorkShift(pills); + // 获取常量行跨度 + const baseSpan = this.constructor.GROUP_ROW_SPAN; + Object.keys(groupPills).forEach(key => { + const aggregatePills = this.aggregateWorkShiftPills(groupPills[key].pills); + aggregatePills.forEach(pill => { + pill.aggregateValue = pill.pills.length; + pill.id = `__pill__${this.nextPillId++}`; + pill.grid = { + ...pill.grid, + row: [1, baseSpan], // 设置网格位置 + }; + }) + groupPills[key].pills = aggregatePills; + }); + // 填充模板信息 + this.fetchSlotTemplate(groupPills); + return Object.values(groupPills); + }, +}); diff --git a/planning_gantt/static/src/xml/planning_gantt_renderer.xml b/planning_gantt/static/src/xml/planning_gantt_renderer.xml new file mode 100644 index 000000000..d8022cd14 --- /dev/null +++ b/planning_gantt/static/src/xml/planning_gantt_renderer.xml @@ -0,0 +1,53 @@ + + + + + +
+ + +
+
+ +
+
+
+
+
+
+
+ + + + + +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+
+
+ +
+ + + diff --git a/planning_gantt/views/plan_views.xml b/planning_gantt/views/plan_views.xml new file mode 100644 index 000000000..7c42bf474 --- /dev/null +++ b/planning_gantt/views/plan_views.xml @@ -0,0 +1,16 @@ + + + + + planning.slot.gantt.inherit + planning.slot + + + + + + + + + + \ No newline at end of file diff --git a/yuthon_hr_attendance/views/inherit_planning_role_views.xml b/yuthon_hr_attendance/views/inherit_planning_role_views.xml index f4c1517f4..3b3d53d6a 100644 --- a/yuthon_hr_attendance/views/inherit_planning_role_views.xml +++ b/yuthon_hr_attendance/views/inherit_planning_role_views.xml @@ -13,28 +13,4 @@ - - 班次名称 - planning.role - tree - [('department_id.name','=','保安部')] - - - 班次名称 - planning.role - tree - [('department_id.name', '=', '工程部')] - - - 班次名称 - planning.role - tree - [('department_id.name', '=', '联安粮库')] - - - 班次名称 - planning.role - tree - [('department_id.name', '=', '客服中心')] - \ No newline at end of file diff --git a/yuthon_hr_attendance/views/inherit_planning_slot_template_views.xml b/yuthon_hr_attendance/views/inherit_planning_slot_template_views.xml index dbeaebc77..ec720c2b3 100644 --- a/yuthon_hr_attendance/views/inherit_planning_slot_template_views.xml +++ b/yuthon_hr_attendance/views/inherit_planning_slot_template_views.xml @@ -27,28 +27,4 @@ - - 班次模板 - planning.slot.template - tree,form - [('department_id.name','in',['保安部', '班组A', '班组B','班组C'])] - - - 班次模板 - planning.slot.template - tree,form - [('department_id.name', '=', '工程部')] - - - 班次模板 - planning.slot.template - tree,form - [('department_id.name', '=', '联安粮库')] - - - 班次模板 - planning.slot.template - tree,form - [('department_id.name', '=', '客服中心')] - \ No newline at end of file diff --git a/yuthon_hr_employee/models/inherit_res_groups.py b/yuthon_hr_employee/models/inherit_res_groups.py index 6f82f9b50..b2f3f74a8 100644 --- a/yuthon_hr_employee/models/inherit_res_groups.py +++ b/yuthon_hr_employee/models/inherit_res_groups.py @@ -6,5 +6,11 @@ import datetime class ResGroups(models.Model): _inherit = "res.groups" + users_ids = fields.Many2many('res.users', string="权限用户") + def users_unlink(self): - self.users.search([('name', '!=', 'Administrator')]).unlink() \ No newline at end of file + users_to_unlink = self.users.filtered(lambda user: user.name != 'Administrator') + self.write({'users': [(3, user.id) for user in users_to_unlink]}) + + def users_add(self): + self.write({'users': [(4, user.id) for user in self.users_ids]}) \ No newline at end of file diff --git a/yuthon_hr_employee/views/inherit_res_groups_views.xml b/yuthon_hr_employee/views/inherit_res_groups_views.xml index 77ddf7ad4..af598c767 100644 --- a/yuthon_hr_employee/views/inherit_res_groups_views.xml +++ b/yuthon_hr_employee/views/inherit_res_groups_views.xml @@ -5,8 +5,12 @@ res.groups - -