mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[IMP] timesheet_grid_work_entry: dynamic ui for project task timer
WIP for grid views overrides to add work_type_id
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import wizard
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Timesheet Grid Work Entry',
|
||||
'description': 'bridge',
|
||||
'version': '15.0.1.0.0',
|
||||
'version': '15.0.1.0.1',
|
||||
'website': 'https://hibou.io/',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'license': 'AGPL-3',
|
||||
'license': 'OPL-1',
|
||||
'category': 'Human Resources',
|
||||
'depends': [
|
||||
'hr_timesheet_work_entry',
|
||||
@@ -12,9 +14,22 @@
|
||||
],
|
||||
'data': [
|
||||
'views/timesheet_views.xml',
|
||||
'wizard/project_task_create_timesheet_views.xml',
|
||||
'wizard/timesheet_merge_wizard_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
# 'timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_renderer.js',
|
||||
# 'timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_view.js',
|
||||
# 'timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_m2o.js',
|
||||
# 'timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_header_component.js',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
# 'timesheet_grid_work_entry/static/src/xml/**/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'application': False,
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
odoo.define('timesheet_grid_work_entry.TimerHeaderComponent', function (require) {
|
||||
"use strict";
|
||||
|
||||
console.log('timesheet_grid_work_entry.TimerHeaderComponent v1');
|
||||
|
||||
const fieldUtils = require('web.field_utils');
|
||||
const TimerHeaderM2O = require('timesheet_grid_work_entry.TimerHeaderM2O');
|
||||
const TimerHeaderComponent = require('timesheet_grid.TimerHeaderComponent');
|
||||
|
||||
const { useState, useRef } = owl.hooks;
|
||||
const { ComponentAdapter } = require('web.OwlCompatibility');
|
||||
|
||||
class TimerHeaderM2OAdapter extends ComponentAdapter {
|
||||
async updateWidget(nextProps) {
|
||||
console.log(nextProps);
|
||||
if (this.widget.workTypeId !== nextProps.widgetArgs[2]) {
|
||||
this.widget.workTypeId = nextProps.widgetArgs[2];
|
||||
const workType = this.widget.workTypeId || false;
|
||||
await this.widget.workTypeMany2one.reinitialize(workType);
|
||||
}
|
||||
// Original
|
||||
if (this.widget.projectId !== nextProps.widgetArgs[0] ||
|
||||
this.widget.taskId !== nextProps.widgetArgs[1]) {
|
||||
this.widget.projectId = nextProps.widgetArgs[0];
|
||||
this.widget.taskId = nextProps.widgetArgs[1];
|
||||
this.widget._updateRequiredField();
|
||||
const project = this.widget.projectId || false;
|
||||
await this.widget.projectMany2one.reinitialize(project);
|
||||
this.widget.taskMany2one.field.domain = [['project_id', '=?', project]];
|
||||
const task = this.widget.taskId || false;
|
||||
await this.widget.taskMany2one.reinitialize(task);
|
||||
} else if (nextProps.widgetArgs[3]) {
|
||||
this.widget._updateRequiredField();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimerHeaderComponent.props['workTypeId'] = {
|
||||
type: Number,
|
||||
optional: true
|
||||
};
|
||||
TimerHeaderComponent.props['workTypeName'] = {
|
||||
type: String,
|
||||
optional: true
|
||||
};
|
||||
TimerHeaderComponent.components = { TimerHeaderM2OAdapter };
|
||||
|
||||
class TimerHeaderComponentWorkEntry extends TimerHeaderComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.TimerHeaderM2O = TimerHeaderM2O;
|
||||
}
|
||||
}
|
||||
|
||||
return TimerHeaderComponentWorkEntry;
|
||||
|
||||
// class TimerHeaderComponent extends owl.Component {
|
||||
// constructor() {
|
||||
// super(...arguments);
|
||||
|
||||
// this.state = useState({
|
||||
// time: null,
|
||||
// manualTimeInput: false,
|
||||
// errorManualTimeInput: false,
|
||||
// });
|
||||
// this.TimerHeaderM2O = TimerHeaderM2O;
|
||||
// this.manualTimerAmount = "00:00";
|
||||
// this.manualTimeInput = useRef("manualTimerInput");
|
||||
// this.descriptionInput = useRef("inputDescription");
|
||||
// this.startButton = useRef("startButton");
|
||||
// this.stopButton = useRef("stopButton");
|
||||
// this.timerStarted = false;
|
||||
|
||||
// if (this.props.timerRunning === true) {
|
||||
// this.timerStarted = true;
|
||||
// this.state.time = Math.floor(Date.now() / 1000) - this.props.timer;
|
||||
// this.timer = setInterval(() => {
|
||||
// this.state.time = Math.floor(Date.now() / 1000) - this.props.timer;
|
||||
// }, 1000);
|
||||
// }
|
||||
// }
|
||||
// async willUpdateProps(nextProps) {
|
||||
// if (nextProps.description !== this.props.description && this.descriptionInput.el) {
|
||||
// this.descriptionInput.el.value = nextProps.description;
|
||||
// }
|
||||
// return super.willUpdateProps(...arguments);
|
||||
// }
|
||||
// patched() {
|
||||
// if (this.state.manualTimeInput && !this.state.errorManualTimeInput && this.manualTimeInput.el !== document.activeElement) {
|
||||
// this.manualTimeInput.el.focus();
|
||||
// this.manualTimeInput.el.select();
|
||||
// }
|
||||
// if (this.props.timerRunning && !this.timerStarted) {
|
||||
// this.timerStarted = true;
|
||||
// this.state.time = Math.floor(Date.now() / 1000) - this.props.timer;
|
||||
// this.timer = setInterval(() => {
|
||||
// this.state.time = Math.floor(Date.now() / 1000) - this.props.timer;
|
||||
// }, 1000);
|
||||
// this.stopButton.el.focus();
|
||||
// } else if (!this.props.timerRunning && this.timerStarted) {
|
||||
// this.timerStarted = false;
|
||||
// clearInterval(this.timer);
|
||||
// this.startButton.el.focus();
|
||||
// }
|
||||
// }
|
||||
// mounted() {
|
||||
// if (this.stopButton.el) {
|
||||
// this.stopButton.el.focus();
|
||||
// } else {
|
||||
// this.startButton.el.focus();
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// //----------------------------------------------------------------------
|
||||
// // Getters
|
||||
// //----------------------------------------------------------------------
|
||||
|
||||
// get timerMode() {
|
||||
// return this.props.addTimeMode;
|
||||
// }
|
||||
// get timeInput() {
|
||||
// return this.manualTimerAmount;
|
||||
// }
|
||||
// get _timerIsRunning() {
|
||||
// return this.props.timerRunning;
|
||||
// }
|
||||
// get _timerReadOnly() {
|
||||
// return this.props.timerReadOnly;
|
||||
// }
|
||||
// get _manualTimeInput() {
|
||||
// return this.state.manualTimeInput;
|
||||
// }
|
||||
// get _timerString() {
|
||||
// if (this.state.time) {
|
||||
// const hours = Math.floor(this.state.time / 3600);
|
||||
// const secondsLeft = this.state.time % 3600;
|
||||
// const seconds = this._display2digits(secondsLeft % 60);
|
||||
// const minutes = this._display2digits(Math.floor(secondsLeft / 60));
|
||||
|
||||
// return `${this._display2digits(hours)}:${minutes}:${seconds}`;
|
||||
// }
|
||||
// return "00:00:00";
|
||||
// }
|
||||
// get isMobile() {
|
||||
// return this.env.device.isMobile;
|
||||
// }
|
||||
|
||||
// //--------------------------------------------------------------------------
|
||||
// // Private
|
||||
// //--------------------------------------------------------------------------
|
||||
|
||||
// _display2digits(number) {
|
||||
// return number > 9 ? "" + number : "0" + number;
|
||||
// }
|
||||
|
||||
// //--------------------------------------------------------------------------
|
||||
// // Handlers
|
||||
// //--------------------------------------------------------------------------
|
||||
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {KeyboardEvent} ev
|
||||
// */
|
||||
// async _onKeydown(ev) {
|
||||
// if (ev.key === 'Enter') {
|
||||
// ev.preventDefault();
|
||||
// if (this.state.manualTimeInput) {
|
||||
// this._onFocusoutTimer(ev);
|
||||
// } else {
|
||||
// this.trigger('new-description', ev.target.value);
|
||||
// this.trigger('timer-stopped');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {MouseEvent} ev
|
||||
// */
|
||||
// _onFocusoutTimer(ev) {
|
||||
// try {
|
||||
// const value = fieldUtils.parse['float_time'](ev.target.value);
|
||||
// this.state.errorManualTimeInput = (value < 0);
|
||||
// if (!this.state.errorManualTimeInput) {
|
||||
// this.trigger('new-timer-value', value);
|
||||
// this.state.time = value * 3600;
|
||||
// this.state.manualTimeInput = false;
|
||||
// }
|
||||
// } catch (_) {
|
||||
// this.state.errorManualTimeInput = true;
|
||||
// }
|
||||
// }
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {KeyboardEvent} ev
|
||||
// */
|
||||
// _onInputTimer(ev) {
|
||||
// try {
|
||||
// const value = fieldUtils.parse['float_time'](ev.target.value);
|
||||
// this.state.errorManualTimeInput = (value < 0);
|
||||
// } catch (_) {
|
||||
// this.state.errorManualTimeInput = true;
|
||||
// }
|
||||
// }
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {MouseEvent} ev
|
||||
// */
|
||||
// _onClickStopTimer(ev) {
|
||||
// ev.stopPropagation();
|
||||
// this.trigger('timer-stopped');
|
||||
// }
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {MouseEvent} ev
|
||||
// */
|
||||
// _onClickStartTimer(ev) {
|
||||
// ev.stopPropagation();
|
||||
// this.trigger('timer-started');
|
||||
// }
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {Event} ev
|
||||
// */
|
||||
// _onInputDescription(ev) {
|
||||
// this.trigger('new-description', ev.target.value);
|
||||
// }
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {MouseEvent} ev
|
||||
// */
|
||||
// async _onClickManualTime(ev) {
|
||||
// if (this.props.timerReadOnly) {
|
||||
// return;
|
||||
// }
|
||||
// const rounded_minutes = await this.rpc({
|
||||
// model: 'account.analytic.line',
|
||||
// method: 'get_rounded_time',
|
||||
// args: [this.state.time/60],
|
||||
// });
|
||||
// this.manualTimerAmount = fieldUtils.format['float_time'](rounded_minutes);
|
||||
// this.state.manualTimeInput = true;
|
||||
// }
|
||||
// /**
|
||||
// * @private
|
||||
// * @param {MouseEvent} ev
|
||||
// */
|
||||
// async _onUnlinkTimer(ev) {
|
||||
// this.trigger('timer-unlink');
|
||||
// }
|
||||
// }
|
||||
// TimerHeaderComponent.template = 'timesheet_grid.timer_header';
|
||||
// TimerHeaderComponent.props = {
|
||||
// taskId: {
|
||||
// type: Number,
|
||||
// optional: true
|
||||
// },
|
||||
// projectId: {
|
||||
// type: Number,
|
||||
// optional: true
|
||||
// },
|
||||
// taskName: {
|
||||
// type: String,
|
||||
// optional: true
|
||||
// },
|
||||
// projectName: {
|
||||
// type: String,
|
||||
// optional: true
|
||||
// },
|
||||
// stepTimer: Number,
|
||||
// timer: Number,
|
||||
// description: {
|
||||
// type: String,
|
||||
// optional: true
|
||||
// },
|
||||
// timerRunning: Boolean,
|
||||
// addTimeMode: Boolean,
|
||||
// timerReadOnly: {
|
||||
// type: Boolean,
|
||||
// optional: true
|
||||
// },
|
||||
// projectWarning: Boolean,
|
||||
// };
|
||||
// TimerHeaderComponent.components = { TimerHeaderM2OAdapter };
|
||||
|
||||
// return TimerHeaderComponent;
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
odoo.define('timesheet_grid_work_entry.TimerHeaderM2O', function (require) {
|
||||
"use strict";
|
||||
|
||||
console.log('timesheet_grid_work_entry.TimerHeaderM2OWorkEntry v1');
|
||||
|
||||
const config = require('web.config');
|
||||
const core = require('web.core');
|
||||
const relational_fields = require('web.relational_fields');
|
||||
const Widget = require('web.Widget');
|
||||
|
||||
const Many2One = relational_fields.FieldMany2One;
|
||||
const _t = core._t;
|
||||
const TimerHeaderM2O = require('timesheet_grid.TimerHeaderM2O');
|
||||
|
||||
const TimerHeaderM2OWorkEntry = Widget.include(TimerHeaderM2O, {
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Widget} parent
|
||||
* @param {Object} params
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
console.log('TimerHeaderM2OWorkEntry.init v1');
|
||||
this._super(...arguments);
|
||||
// StandaloneFieldManagerMixin.init.call(this);
|
||||
// this.projectId = arguments[1];
|
||||
// this.taskId = arguments[2];
|
||||
this.workTypeId = arguments[3];
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: async function () {
|
||||
await this._super(...arguments);
|
||||
|
||||
const workTypeDomain = [['allow_timesheet', '=', true]];
|
||||
this.workType = await this.model.makeRecord('account.analytic.line', [{
|
||||
name: 'work_type_id',
|
||||
relation: 'hr.work.entry.type',
|
||||
type: 'many2one',
|
||||
value: this.projectTypeId,
|
||||
domain: workTypeDomain,
|
||||
}]);
|
||||
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: async function () {
|
||||
const _super = this._super.bind(this);
|
||||
let placeholderWorkType;
|
||||
|
||||
if (config.device.isMobile) {
|
||||
placeholderWorkType = _t('Work Type');
|
||||
} else {
|
||||
placeholderWorkType = _t('Select a Work Type');
|
||||
}
|
||||
const workTypeRecord = this.model.get(this.workType);
|
||||
const workTypeMany2one = new Many2One(this, 'work_type_id', workTypeRecord, {
|
||||
attrs: {
|
||||
placeholder: placeholderWorkType,
|
||||
},
|
||||
noOpen: true,
|
||||
noCreate: true,
|
||||
mode: 'edit',
|
||||
required: true,
|
||||
});
|
||||
workTypeMany2one.field['required'] = true;
|
||||
this._registerWidget(this.workType, 'work_type_id', workTypeMany2one);
|
||||
await workTypeMany2one.appendTo(this.$('.timer_work_type_id'));
|
||||
this.workTypeMany2one = workTypeMany2one;
|
||||
|
||||
_super.apply(...arguments);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onFieldChanged: async function (ev) {
|
||||
await this._super(...arguments);
|
||||
|
||||
const workType = this.workTypeId;
|
||||
const fieldName = ev.target.name;
|
||||
|
||||
if (fieldName === 'work_type_id') {
|
||||
record = this.model.get(this.workType);
|
||||
var newId = record.data.work_type_id.res_id;
|
||||
if (workType !== newId) {
|
||||
this.workTypeId = newId;
|
||||
}
|
||||
}
|
||||
|
||||
// } else if (fieldName === 'task_id') {
|
||||
// record = this.model.get(this.task);
|
||||
// const newId = record.data.task_id && record.data.task_id.res_id;
|
||||
// if (task !== newId) {
|
||||
// let project_id = this.projectId;
|
||||
// if (!project_id) {
|
||||
// const task_data = await this._rpc({
|
||||
// model: 'project.task',
|
||||
// method: 'search_read',
|
||||
// args: [[['id', '=', newId]], ['project_id']],
|
||||
// });
|
||||
// project_id = task_data[0].project_id[0];
|
||||
// }
|
||||
|
||||
// this.taskId = false;
|
||||
// this.trigger_up('timer-edit-task', {'taskId': newId, 'projectId': project_id});
|
||||
// }
|
||||
// }
|
||||
},
|
||||
});
|
||||
|
||||
return TimerHeaderM2OWorkEntry
|
||||
|
||||
});
|
||||
@@ -0,0 +1,357 @@
|
||||
odoo.define('timesheet_grid_work_entry.TimerGridRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
console.log('timesheet_grid_work_entry.TimerGridRenderer v1');
|
||||
|
||||
const utils = require('web.utils');
|
||||
const GridRenderer = require('web_grid.GridRenderer');
|
||||
const TimerHeaderComponent = require('timesheet_grid_work_entry.TimerHeaderComponent');
|
||||
const TimerStartComponent = require('timesheet_grid.TimerStartComponent');
|
||||
const { useState, useExternalListener, useRef } = owl.hooks;
|
||||
|
||||
class TimerGridRenderer extends GridRenderer {
|
||||
constructor(parent, props) {
|
||||
super(...arguments);
|
||||
useExternalListener(window, 'keydown', this._onKeydown);
|
||||
useExternalListener(window, 'keyup', this._onKeyup);
|
||||
|
||||
this.initialGridAnchor = props.context.grid_anchor;
|
||||
this.initialGroupBy = props.groupBy;
|
||||
|
||||
this.stateTimer = useState({
|
||||
taskId: undefined,
|
||||
taskName: '',
|
||||
projectId: undefined,
|
||||
projectName: '',
|
||||
workTypeId: undefined,
|
||||
workTypeName: '',
|
||||
addTimeMode: false,
|
||||
description: '',
|
||||
startSeconds: 0,
|
||||
timerRunning: false,
|
||||
indexRunning: -1,
|
||||
readOnly: false,
|
||||
projectWarning: false,
|
||||
});
|
||||
this.timerHeader = useRef('timerHeader');
|
||||
this.timesheetId = false;
|
||||
this._onChangeProjectTaskDebounce = _.debounce(this._setProjectTask.bind(this), 500);
|
||||
}
|
||||
mounted() {
|
||||
super.mounted(...arguments);
|
||||
if (this.formatType === 'float_time') {
|
||||
this._get_running_timer();
|
||||
}
|
||||
}
|
||||
async willUpdateProps(nextProps) {
|
||||
if (nextProps.data !== this.props.data) {
|
||||
this._match_line(nextProps.data);
|
||||
}
|
||||
return super.willUpdateProps(...arguments);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Getters
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {boolean} returns true if when we need to display the timer button
|
||||
*
|
||||
*/
|
||||
get showTimerButton() {
|
||||
return ((this.formatType === 'float_time') && (
|
||||
this.props.groupBy.includes('project_id')
|
||||
));
|
||||
}
|
||||
/**
|
||||
* @returns {boolean} returns always true if timesheet in hours, that way we know we're on a timesheet grid and
|
||||
* we can show the timer header.
|
||||
*
|
||||
*/
|
||||
get showTimer() {
|
||||
return this.formatType === 'float_time';
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_match_line(new_grid) {
|
||||
const grid = (new_grid) ? new_grid[0].rows : this.props.data[0].rows;
|
||||
let current_value;
|
||||
for (let i = 0; i < grid.length; i++) {
|
||||
current_value = grid[i].values;
|
||||
if (current_value.project_id && current_value.project_id[0] === this.stateTimer.projectId
|
||||
&& ((!current_value.task_id && !this.stateTimer.taskId) ||
|
||||
(current_value.task_id && current_value.task_id[0] === this.stateTimer.taskId))) {
|
||||
this.stateTimer.indexRunning = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.stateTimer.indexRunning = -1;
|
||||
}
|
||||
async _get_running_timer() {
|
||||
const result = await this.rpc({
|
||||
model: 'account.analytic.line',
|
||||
method: 'get_running_timer',
|
||||
args: []
|
||||
});
|
||||
if (result.id !== undefined) {
|
||||
this.stateTimer.timerRunning = true;
|
||||
this.timesheetId = result.id;
|
||||
this.stateTimer.readOnly = result.readonly;
|
||||
this.stateTimer.projectId = result.project_id;
|
||||
this.stateTimer.taskId = result.task_id || undefined;
|
||||
|
||||
// In case of read-only timer
|
||||
this.stateTimer.projectName = (result.project_name) ? result.project_name : '';
|
||||
this.stateTimer.taskName = (result.task_name) ? result.task_name : '';
|
||||
|
||||
this.stateTimer.timerRunning = true;
|
||||
this.stateTimer.description = (result.description === '/') ? '' : result.description;
|
||||
this.stateTimer.startSeconds = Math.floor(Date.now() / 1000) - result.start;
|
||||
} else if (this.stateTimer.timerRunning && this.stateTimer.projectId) {
|
||||
this.timesheetId = false;
|
||||
this.stateTimer.readOnly = false;
|
||||
this.stateTimer.projectId = false;
|
||||
this.stateTimer.taskId = undefined;
|
||||
|
||||
this.stateTimer.timerRunning = false;
|
||||
this.stateTimer.description = '';
|
||||
}
|
||||
if (this.timerHeader.comp.startButton.el) {
|
||||
this.timerHeader.comp.startButton.el.focus();
|
||||
}
|
||||
this._match_line();
|
||||
}
|
||||
async _onSetProject(data) {
|
||||
this.stateTimer.projectId = data.detail.projectId;
|
||||
this.stateTimer.taskId = undefined;
|
||||
this._onChangeProjectTaskDebounce(data.detail.projectId, undefined);
|
||||
}
|
||||
async _onSetWorkType(data) {
|
||||
this.stateTimer.workTypeId = data.detail.workTypeId;
|
||||
}
|
||||
async _onSetTask(data) {
|
||||
this.stateTimer.projectId = data.detail.projectId;
|
||||
this.stateTimer.taskId = data.detail.taskId || undefined;
|
||||
this._onChangeProjectTaskDebounce(this.stateTimer.projectId, data.detail.taskId);
|
||||
}
|
||||
async _setProjectTask(projectId, taskId) {
|
||||
if (!this.stateTimer.projectId) {
|
||||
return;
|
||||
}
|
||||
if (this.timesheetId) {
|
||||
const timesheetId = await this.rpc({
|
||||
model: 'account.analytic.line',
|
||||
method: 'action_change_project_task',
|
||||
args: [[this.timesheetId], this.stateTimer.projectId, this.stateTimer.taskId],
|
||||
});
|
||||
if (this.timesheetId !== timesheetId) {
|
||||
this.timesheetId = timesheetId;
|
||||
await this._get_running_timer();
|
||||
}
|
||||
} else {
|
||||
const seconds = Math.floor(Date.now() / 1000) - this.stateTimer.startSeconds;
|
||||
this.timesheetId = await this.rpc({
|
||||
model: 'account.analytic.line',
|
||||
method: 'create',
|
||||
args: [{
|
||||
'name': this.stateTimer.description,
|
||||
'project_id': this.stateTimer.projectId,
|
||||
'task_id': this.stateTimer.taskId,
|
||||
}],
|
||||
});
|
||||
// Add already runned time and start timer if doesn't running yet in DB
|
||||
this.trigger('add_time_timer', {
|
||||
timesheetId: this.timesheetId,
|
||||
time: seconds
|
||||
});
|
||||
}
|
||||
this._match_line();
|
||||
}
|
||||
async _onClickLineButton(taskId, projectId) {
|
||||
// Check that we can create timers for the selected project.
|
||||
// This is an edge case in multi-company environment.
|
||||
const canStartTimerResult = await this.rpc({
|
||||
model: 'project.project',
|
||||
method: 'check_can_start_timer',
|
||||
args: [[projectId]],
|
||||
});
|
||||
if (canStartTimerResult !== true) {
|
||||
this.trigger('do_action', {action: canStartTimerResult})
|
||||
return;
|
||||
}
|
||||
if (this.stateTimer.addTimeMode === true) {
|
||||
this.timesheetId = await this.rpc({
|
||||
model: 'account.analytic.line',
|
||||
method: 'action_add_time_to_timesheet',
|
||||
args: [[this.timesheetId], projectId, taskId, this.props.stepTimer * 60],
|
||||
});
|
||||
this.trigger('update_timer');
|
||||
} else if (! this.timesheetId && this.stateTimer.timerRunning) {
|
||||
this.stateTimer.projectId = projectId;
|
||||
this.stateTimer.taskId = (taskId) ? taskId : undefined;
|
||||
await this._onChangeProjectTaskDebounce(projectId, taskId);
|
||||
} else {
|
||||
if (this.stateTimer.projectId === projectId && this.stateTimer.taskId === taskId) {
|
||||
await this._stop_timer();
|
||||
return;
|
||||
}
|
||||
await this._stop_timer();
|
||||
this.stateTimer.projectId = projectId;
|
||||
this.stateTimer.taskId = (taskId) ? taskId : undefined;
|
||||
await this._onTimerStarted();
|
||||
await this._onChangeProjectTaskDebounce(projectId, taskId);
|
||||
}
|
||||
if (this.timerHeader.comp.stopButton.el) {
|
||||
this.timerHeader.comp.stopButton.el.focus();
|
||||
}
|
||||
}
|
||||
async _onTimerStarted() {
|
||||
this.stateTimer.timerRunning = true;
|
||||
this.stateTimer.addTimeMode = false;
|
||||
this.stateTimer.startSeconds = Math.floor(Date.now() / 1000);
|
||||
if (this.props.defaultProject && ! this.stateTimer.projectId) {
|
||||
this.stateTimer.projectId = this.props.defaultProject;
|
||||
this._onChangeProjectTaskDebounce(this.props.defaultProject, undefined);
|
||||
}
|
||||
}
|
||||
async _stop_timer() {
|
||||
if (!this.timesheetId) {
|
||||
this.stateTimer.projectWarning = true;
|
||||
return;
|
||||
}
|
||||
let timesheetId = this.timesheetId;
|
||||
this.timesheetId = false;
|
||||
this.trigger('stop_timer', {
|
||||
timesheetId: timesheetId,
|
||||
});
|
||||
this.stateTimer.description = '';
|
||||
this.stateTimer.timerRunning = false;
|
||||
this.timesheetId = false;
|
||||
this.stateTimer.projectId = undefined;
|
||||
this.stateTimer.taskId = undefined;
|
||||
|
||||
this.stateTimer.timerRunning = false;
|
||||
this.stateTimer.projectWarning = false;
|
||||
|
||||
this._match_line();
|
||||
this.stateTimer.readOnly = false;
|
||||
}
|
||||
async _onTimerUnlink() {
|
||||
if (this.timesheetId !== false) {
|
||||
this.trigger('unlink_timer', {
|
||||
timesheetId: this.timesheetId,
|
||||
});
|
||||
}
|
||||
this.timesheetId = false;
|
||||
this.stateTimer.projectId = undefined;
|
||||
this.stateTimer.taskId = undefined;
|
||||
|
||||
this.stateTimer.timerRunning = false;
|
||||
this.stateTimer.description = '';
|
||||
this.stateTimer.manualTimeInput = false;
|
||||
this._match_line();
|
||||
this.stateTimer.readOnly = false;
|
||||
this.stateTimer.projectWarning = false;
|
||||
}
|
||||
_onNewDescription(data) {
|
||||
this.stateTimer.description = data.detail;
|
||||
if (this.timesheetId) {
|
||||
this.trigger('update_timer_description', {
|
||||
timesheetId: this.timesheetId,
|
||||
description: data.detail
|
||||
});
|
||||
}
|
||||
}
|
||||
async _onNewTimerValue(data) {
|
||||
const seconds = Math.floor(Date.now() / 1000) - this.stateTimer.startSeconds;
|
||||
const toAdd = data.detail * 3600 - seconds;
|
||||
this.stateTimer.startSeconds = this.stateTimer.startSeconds - toAdd;
|
||||
if (this.timesheetId && typeof toAdd === 'number') {
|
||||
this.trigger('add_time_timer', {
|
||||
timesheetId: this.timesheetId,
|
||||
time: toAdd
|
||||
});
|
||||
}
|
||||
this.timerHeader.comp.stopButton.el.focus();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Handlers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
async _onClickStartTimerFromLine(ev) {
|
||||
if (! ev.detail) {
|
||||
return;
|
||||
}
|
||||
const cell_path = ev.detail.split('.');
|
||||
const grid_path = cell_path.slice(0, -2);
|
||||
const row_path = grid_path.concat(['rows'], cell_path.slice(-1));
|
||||
const row = utils.into(this.props.data, row_path);
|
||||
const data = row.values;
|
||||
const task = (data.task_id) ? data.task_id[0] : undefined;
|
||||
this._onClickLineButton(task, data.project_id[0]);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
async _onKeydown(ev) {
|
||||
if (ev.key === 'Shift' && !this.stateTimer.timerRunning && !this.state.editMode) {
|
||||
this.stateTimer.addTimeMode = true;
|
||||
} else if (!ev.altKey && !ev.ctrlKey && !ev.metaKey && this.showTimerButton && ! ['input', 'textarea'].includes(ev.target.tagName.toLowerCase())) {
|
||||
if (ev.key === 'Escape' && this.stateTimer.timerRunning) {
|
||||
this._onTimerUnlink();
|
||||
}
|
||||
const index = ev.keyCode - 65;
|
||||
if (index >= 0 && index <= 26 && index < this.props.data[0].rows.length) {
|
||||
const data = this.props.data[0].rows[index].values;
|
||||
const projectId = data.project_id[0];
|
||||
const taskId = data.task_id && data.task_id[0];
|
||||
this._onClickLineButton(taskId, projectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeyup(ev) {
|
||||
if (ev.key === 'Shift' && !this.state.editMode) {
|
||||
this.stateTimer.addTimeMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimerGridRenderer.props = Object.assign({}, GridRenderer.props, {
|
||||
serverTime: {
|
||||
type: String,
|
||||
optional: true
|
||||
},
|
||||
stepTimer: {
|
||||
type: Number,
|
||||
optional: true
|
||||
},
|
||||
defaultProject: {
|
||||
type: [Boolean, Number],
|
||||
optional: true
|
||||
},
|
||||
Component: {
|
||||
type: Object,
|
||||
optional: true
|
||||
},
|
||||
});
|
||||
|
||||
TimerGridRenderer.components = {
|
||||
TimerHeaderComponent,
|
||||
TimerStartComponent,
|
||||
};
|
||||
|
||||
return TimerGridRenderer;
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
odoo.define('timesheet_grid_work_entry.TimerGridView', function (require) {
|
||||
"use strict";
|
||||
|
||||
console.log('timesheet_grid_work_entry.TimerGridView v1');
|
||||
|
||||
const viewRegistry = require('web.view_registry');
|
||||
const WebGridView = require('web_grid.GridView');
|
||||
const TimerGridController = require('timesheet_grid.TimerGridController');
|
||||
const TimerGridModel = require('timesheet_grid.TimerGridModel');
|
||||
const GridRenderer = require('timesheet_grid_work_entry.TimerGridRenderer');
|
||||
const TimesheetConfigQRCodeMixin = require('timesheet_grid.TimesheetConfigQRCodeMixin');
|
||||
const { onMounted, onPatched } = owl.hooks;
|
||||
|
||||
class TimerGridRenderer extends GridRenderer {
|
||||
constructor() {
|
||||
console.log('TimerGridRenderer constructor called');
|
||||
super(...arguments);
|
||||
onMounted(() => this._bindPlayStoreIcon());
|
||||
onPatched(() => this._bindPlayStoreIcon());
|
||||
}
|
||||
}
|
||||
|
||||
// QRCode mixin to bind event on play store icon
|
||||
Object.assign(TimerGridRenderer.prototype, TimesheetConfigQRCodeMixin);
|
||||
|
||||
const TimerGridView = WebGridView.extend({
|
||||
config: Object.assign({}, WebGridView.prototype.config, {
|
||||
Model: TimerGridModel,
|
||||
Controller: TimerGridController,
|
||||
Renderer: TimerGridRenderer
|
||||
})
|
||||
});
|
||||
|
||||
viewRegistry.add('timesheet_timer_grid', TimerGridView);
|
||||
|
||||
return TimerGridView;
|
||||
});
|
||||
15
timesheet_grid_work_entry/static/src/xml/timer_m2o.xml
Normal file
15
timesheet_grid_work_entry/static/src/xml/timer_m2o.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Refactor this into widgets.xml for consistency ? -->
|
||||
|
||||
<templates>
|
||||
|
||||
<template t-name="timesheet_grid_work_entry.timer_project_task" t-inherit="timesheet_grid.timer_project_task" class="d-inline-flex timer_m2o">
|
||||
<xpath expr="//div[hasclass('px-2')]" position="after">
|
||||
<div class="px-2 flex-grow-1">
|
||||
<div class="timer_work_type_id my-auto"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</templates>
|
||||
23
timesheet_grid_work_entry/static/src/xml/timesheet_grid.xml
Normal file
23
timesheet_grid_work_entry/static/src/xml/timesheet_grid.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="timesheet_grid_work_entry.GridRenderer" t-inherit="web_grid.GridRenderer" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//TimerHeaderComponent" position="attributes">
|
||||
<attribute name="workTypeId">stateTimer.workTypeId</attribute>
|
||||
<attribute name="t-on-timer-edit-work-type">_onSetWorkType</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="timesheet_grid_work_entry.timer_header" t-inherit="timesheet_grid.timer_header" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('input_m2o')]/div" position="after">
|
||||
<div class="px-2">
|
||||
<span t-esc="props.workTypeName"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//TimerHeaderM2OAdapter" position="attributes">
|
||||
<attribute name="Component">TimerHeaderM2OWorkEntry</attribute>
|
||||
<attribute name="widgetArgs">[props.projectId, props.taskId, props.workTypeId, props.projectWarning]</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -14,4 +14,19 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="timesheet_view_grid_inherit" model="ir.ui.view">
|
||||
<field name="name">account.analytic.line.grid.project.inherit</field>
|
||||
<field name="model">account.analytic.line</field>
|
||||
<field name="inherit_id" ref="timesheet_grid.timesheet_view_grid"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='task_id']" position="after">
|
||||
<!-- <field name="work_type_id"
|
||||
type="row"
|
||||
domain="[('allow_timesheet', '=', True)]"
|
||||
/> -->
|
||||
<field name="work_type_id" type="row"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
4
timesheet_grid_work_entry/wizard/__init__.py
Normal file
4
timesheet_grid_work_entry/wizard/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import project_task_create_timesheet
|
||||
from . import timesheet_merge_wizard
|
||||
@@ -0,0 +1,24 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ProjectTaskCreateTimesheet(models.TransientModel):
|
||||
_inherit = 'project.task.create.timesheet'
|
||||
|
||||
work_type_id = fields.Many2one('hr.work.entry.type', string='Work Type',
|
||||
default=lambda self: self.env.ref('hr_timesheet_work_entry.work_input_timesheet',
|
||||
raise_if_not_found=False))
|
||||
|
||||
def save_timesheet(self):
|
||||
"""
|
||||
super() (timesheet_grid.wizard.project_task_create_timesheet)
|
||||
is CLOSED to values modification (builds internally)
|
||||
# It does however expose the created object, so at the cost of an
|
||||
# additional write at flush we can just write here...
|
||||
"""
|
||||
timesheets = super().save_timesheet()
|
||||
timesheets.write({
|
||||
'work_type_id': self.work_type_id.id,
|
||||
})
|
||||
return timesheets
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_task_create_timesheet_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.create.timesheet.wizard.form.inherit</field>
|
||||
<field name="model">project.task.create.timesheet</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.project_task_create_timesheet_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='task_id']" position="after">
|
||||
<field name="work_type_id"
|
||||
domain="[('allow_timesheet', '=', True)]"
|
||||
context="{'default_allow_timesheet': True}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
61
timesheet_grid_work_entry/wizard/timesheet_merge_wizard.py
Normal file
61
timesheet_grid_work_entry/wizard/timesheet_merge_wizard.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class MergeTimesheets(models.TransientModel):
|
||||
_inherit = 'hr_timesheet.merge.wizard'
|
||||
|
||||
work_type_id = fields.Many2one('hr.work.entry.type', string='Work Type')
|
||||
|
||||
@api.constrains('timesheet_ids')
|
||||
def _check_timesheet_ids(self):
|
||||
for wizard in self:
|
||||
if len(set(wizard.timesheet_ids.mapped('work_type_id'))) > 1:
|
||||
raise ValidationError('The timesheets must have the same work type.')
|
||||
super()._check_timesheet_ids()
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super(MergeTimesheets, self).default_get(fields_list)
|
||||
|
||||
if 'timesheet_ids' in fields_list and res.get('timesheet_ids'):
|
||||
timesheets = self.env['account.analytic.line'].browse(res.get('timesheet_ids'))
|
||||
if timesheets and 'work_type_id' in fields_list:
|
||||
res['work_type_id'] = timesheets.mapped('work_type_id.id')[0]
|
||||
|
||||
return res
|
||||
|
||||
def action_merge(self):
|
||||
"""
|
||||
super() (timesheet_grid.wizard.timesheet_merge_wizard.action_merge) is CLOSED
|
||||
to values injection. It is also closed to post-create modification because it
|
||||
returns a closed window instead of an action with the new timesheet's id (e.g. a redirect)
|
||||
|
||||
Thus a direct inline patch...
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
self.env['account.analytic.line'].create({
|
||||
'name': self.name,
|
||||
'date': self.date,
|
||||
'unit_amount': self.unit_amount,
|
||||
'encoding_uom_id': self.encoding_uom_id.id,
|
||||
'project_id': self.project_id.id,
|
||||
'task_id': self.task_id.id,
|
||||
'employee_id': self.employee_id.id,
|
||||
'work_type_id': self.work_type_id.id,
|
||||
})
|
||||
self.timesheet_ids.unlink()
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'message': _("The timesheet entries have successfully been merged."),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="timesheet_merge_wizard_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr_timesheet.merge.wizard.form.inherit</field>
|
||||
<field name="model">hr_timesheet.merge.wizard</field>
|
||||
<field name="inherit_id" ref="timesheet_grid.timesheet_merge_wizard_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='task_id']" position="after">
|
||||
<field name="work_type_id"
|
||||
domain="[('allow_timesheet', '=', True)]"
|
||||
context="{'default_allow_timesheet': True}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user