diff --git a/.theia/launch.json b/.theia/launch.json
index a43ff192..3c9d23f4 100644
--- a/.theia/launch.json
+++ b/.theia/launch.json
@@ -43,6 +43,14 @@
"program": "/opt/odoo/hibou-suite/odoo-reload.py",
"args": [],
"console": "integratedTerminal"
+ },
+ {
+ "name": "Odoo: reload foreground server, Kill Others",
+ "type": "python",
+ "request": "launch",
+ "program": "/opt/odoo/hibou-suite/odoo-reload.py",
+ "args": ["KILL_OTHER"],
+ "console": "integratedTerminal"
}
]
}
diff --git a/.theia/settings.json b/.theia/settings.json
new file mode 100644
index 00000000..720fd1c4
--- /dev/null
+++ b/.theia/settings.json
@@ -0,0 +1,17 @@
+{
+ "workbench.colorTheme": "Hibou Dark",
+ "files.exclude": {
+ "**/.git": true,
+ "**/.svn": true,
+ "**/.hg": true,
+ "**/CVS": true,
+ "**/.DS_Store": true,
+ "**/*.pyc": {"when": "$(basename).py"},
+ "**/__pycache__": true
+ },
+ "files.watcherExclude": {
+ "**/.git/objects/**": true,
+ "**/.git/subtree-cache/**": true,
+ "**/node_modules/**": true
+ }
+}
diff --git a/hr_timesheet_work_entry/__manifest__.py b/hr_timesheet_work_entry/__manifest__.py
index 60ba9dcb..023f4c92 100755
--- a/hr_timesheet_work_entry/__manifest__.py
+++ b/hr_timesheet_work_entry/__manifest__.py
@@ -7,6 +7,7 @@
'license': 'AGPL-3',
'category': 'Human Resources',
'depends': [
+ 'project',
'hr_timesheet',
'hr_work_entry',
],
diff --git a/hr_timesheet_work_entry/views/timesheet_views.xml b/hr_timesheet_work_entry/views/timesheet_views.xml
index c6380521..9ac5d44f 100644
--- a/hr_timesheet_work_entry/views/timesheet_views.xml
+++ b/hr_timesheet_work_entry/views/timesheet_views.xml
@@ -27,4 +27,22 @@
+
+ project.task.form.inherit
+ project.task
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odoo-reload.py b/odoo-reload.py
index c84471ab..a633c8e8 100755
--- a/odoo-reload.py
+++ b/odoo-reload.py
@@ -1,17 +1,31 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
import psutil
import os
import signal
+import sys
PID = 1
PNAME = 'odoo'
+PNAME_PYTHON = ['python']
+PNAME_KILL_OTHER = [PNAME] + PNAME_PYTHON
+KILL_OTHER = sys.argv[1] == 'KILL_OTHER' if len(sys.argv) >= 2 else False
+if KILL_OTHER:
+ print('Will find other Odoo Processes and Kill them.')
+
is_foreground = False
for proc in psutil.process_iter():
try:
process_name = proc.name()
process_id = proc.pid
+ print('Inspecting %s:%s' % (process_id, process_name))
if process_id == PID:
is_foreground = process_name == PNAME
- break
+ if not KILL_OTHER:
+ break
+ if process_id != PID and KILL_OTHER and process_name in PNAME_KILL_OTHER:
+ print('Killing %s:%s' % (process_id, process_name))
+ os.kill(process_id, signal.SIGKILL)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
diff --git a/timesheet_grid_description/__init__.py b/timesheet_grid_description/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/timesheet_grid_description/__manifest__.py b/timesheet_grid_description/__manifest__.py
new file mode 100755
index 00000000..3be668d0
--- /dev/null
+++ b/timesheet_grid_description/__manifest__.py
@@ -0,0 +1,21 @@
+{
+ 'name': 'Timesheet Grid Description',
+ 'description': 'bridge',
+ 'version': '15.0.1.0.0',
+ 'website': 'https://hibou.io/',
+ 'author': 'Hibou Corp. ',
+ 'license': 'AGPL-3',
+ 'category': 'Human Resources',
+ 'depends': [
+ 'timesheet_description',
+ 'timesheet_grid',
+ ],
+ 'data': [
+ 'views/timesheet_views.xml',
+ ],
+ 'demo': [
+ ],
+ 'installable': True,
+ 'auto_install': True,
+ 'application': False,
+}
diff --git a/timesheet_grid_description/views/timesheet_views.xml b/timesheet_grid_description/views/timesheet_views.xml
new file mode 100644
index 00000000..cd194b2a
--- /dev/null
+++ b/timesheet_grid_description/views/timesheet_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ account.analytic.line.form.inherit
+ account.analytic.line
+
+
+
+
+
+
+
+
+
diff --git a/timesheet_grid_work_entry/__init__.py b/timesheet_grid_work_entry/__init__.py
index e69de29b..455a4c33 100644
--- a/timesheet_grid_work_entry/__init__.py
+++ b/timesheet_grid_work_entry/__init__.py
@@ -0,0 +1,3 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import wizard
diff --git a/timesheet_grid_work_entry/__manifest__.py b/timesheet_grid_work_entry/__manifest__.py
index 4bc7115c..d0dc90c9 100755
--- a/timesheet_grid_work_entry/__manifest__.py
+++ b/timesheet_grid_work_entry/__manifest__.py
@@ -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. ',
- '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,
diff --git a/timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_header_component.js b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_header_component.js
new file mode 100644
index 00000000..1f033ce4
--- /dev/null
+++ b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_header_component.js
@@ -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;
+});
diff --git a/timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_m2o.js b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_m2o.js
new file mode 100644
index 00000000..186649a0
--- /dev/null
+++ b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timer_m2o.js
@@ -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
+
+});
diff --git a/timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_renderer.js b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_renderer.js
new file mode 100644
index 00000000..f954d8b8
--- /dev/null
+++ b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_renderer.js
@@ -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;
+});
diff --git a/timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_view.js b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_view.js
new file mode 100644
index 00000000..7425bd3d
--- /dev/null
+++ b/timesheet_grid_work_entry/static/src/js/timesheet_grid/timesheet_timer_grid_view.js
@@ -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;
+});
diff --git a/timesheet_grid_work_entry/static/src/xml/timer_m2o.xml b/timesheet_grid_work_entry/static/src/xml/timer_m2o.xml
new file mode 100644
index 00000000..f913248e
--- /dev/null
+++ b/timesheet_grid_work_entry/static/src/xml/timer_m2o.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/timesheet_grid_work_entry/static/src/xml/timesheet_grid.xml b/timesheet_grid_work_entry/static/src/xml/timesheet_grid.xml
new file mode 100644
index 00000000..586ad555
--- /dev/null
+++ b/timesheet_grid_work_entry/static/src/xml/timesheet_grid.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ stateTimer.workTypeId
+ _onSetWorkType
+
+
+
+
+
+
+
+
+
+
+ TimerHeaderM2OWorkEntry
+ [props.projectId, props.taskId, props.workTypeId, props.projectWarning]
+
+
+
+
diff --git a/timesheet_grid_work_entry/views/timesheet_views.xml b/timesheet_grid_work_entry/views/timesheet_views.xml
index 1c6c2175..8d10bd5d 100644
--- a/timesheet_grid_work_entry/views/timesheet_views.xml
+++ b/timesheet_grid_work_entry/views/timesheet_views.xml
@@ -14,4 +14,19 @@
+
+ account.analytic.line.grid.project.inherit
+ account.analytic.line
+
+
+
+
+
+
+
+
+
diff --git a/timesheet_grid_work_entry/wizard/__init__.py b/timesheet_grid_work_entry/wizard/__init__.py
new file mode 100644
index 00000000..0f1e5dc2
--- /dev/null
+++ b/timesheet_grid_work_entry/wizard/__init__.py
@@ -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
diff --git a/timesheet_grid_work_entry/wizard/project_task_create_timesheet.py b/timesheet_grid_work_entry/wizard/project_task_create_timesheet.py
new file mode 100644
index 00000000..111583d7
--- /dev/null
+++ b/timesheet_grid_work_entry/wizard/project_task_create_timesheet.py
@@ -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
diff --git a/timesheet_grid_work_entry/wizard/project_task_create_timesheet_views.xml b/timesheet_grid_work_entry/wizard/project_task_create_timesheet_views.xml
new file mode 100644
index 00000000..d8a68084
--- /dev/null
+++ b/timesheet_grid_work_entry/wizard/project_task_create_timesheet_views.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ project.task.create.timesheet.wizard.form.inherit
+ project.task.create.timesheet
+
+
+
+
+
+
+
+
+
diff --git a/timesheet_grid_work_entry/wizard/timesheet_merge_wizard.py b/timesheet_grid_work_entry/wizard/timesheet_merge_wizard.py
new file mode 100644
index 00000000..70a091bb
--- /dev/null
+++ b/timesheet_grid_work_entry/wizard/timesheet_merge_wizard.py
@@ -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'},
+ }
+ }
diff --git a/timesheet_grid_work_entry/wizard/timesheet_merge_wizard_views.xml b/timesheet_grid_work_entry/wizard/timesheet_merge_wizard_views.xml
new file mode 100644
index 00000000..a9d175b7
--- /dev/null
+++ b/timesheet_grid_work_entry/wizard/timesheet_merge_wizard_views.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ hr_timesheet.merge.wizard.form.inherit
+ hr_timesheet.merge.wizard
+
+
+
+
+
+
+
+
+