[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:
Jared Kipe
2022-01-09 14:28:47 -08:00
parent aa534c1aec
commit bb5784f7d9
14 changed files with 996 additions and 2 deletions

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import wizard

View File

@@ -1,10 +1,12 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{ {
'name': 'Timesheet Grid Work Entry', 'name': 'Timesheet Grid Work Entry',
'description': 'bridge', 'description': 'bridge',
'version': '15.0.1.0.0', 'version': '15.0.1.0.1',
'website': 'https://hibou.io/', 'website': 'https://hibou.io/',
'author': 'Hibou Corp. <hello@hibou.io>', 'author': 'Hibou Corp. <hello@hibou.io>',
'license': 'AGPL-3', 'license': 'OPL-1',
'category': 'Human Resources', 'category': 'Human Resources',
'depends': [ 'depends': [
'hr_timesheet_work_entry', 'hr_timesheet_work_entry',
@@ -12,9 +14,22 @@
], ],
'data': [ 'data': [
'views/timesheet_views.xml', 'views/timesheet_views.xml',
'wizard/project_task_create_timesheet_views.xml',
'wizard/timesheet_merge_wizard_views.xml',
], ],
'demo': [ '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, 'installable': True,
'auto_install': True, 'auto_install': True,
'application': False, 'application': False,

View File

@@ -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;
});

View File

@@ -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
});

View File

@@ -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;
});

View File

@@ -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;
});

View 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>

View 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>

View File

@@ -14,4 +14,19 @@
</field> </field>
</record> </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> </odoo>

View 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

View File

@@ -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

View File

@@ -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>

View 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'},
}
}

View File

@@ -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>