diff --git a/addons/hr_timesheet/controllers/portal.py b/addons/hr_timesheet/controllers/portal.py index ec07c343ef2..30e61e4f2ec 100644 --- a/addons/hr_timesheet/controllers/portal.py +++ b/addons/hr_timesheet/controllers/portal.py @@ -33,9 +33,9 @@ class TimesheetCustomerPortal(CustomerPortal): 'name': {'input': 'name', 'label': _('Search in Description')}, } - def _task_get_searchbar_sortings(self): - values = super()._task_get_searchbar_sortings() - values['progress'] = {'label': _('Progress'), 'order': 'progress asc', 'sequence': 9} + def _task_get_searchbar_sortings(self, milestones_allowed): + values = super()._task_get_searchbar_sortings(milestones_allowed) + values['progress'] = {'label': _('Progress'), 'order': 'progress asc', 'sequence': 10} return values def _get_searchbar_groupby(self): diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 1d470038160..b2d06b2ecc8 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -220,25 +220,6 @@ class Project(models.Model): warning_msg, self.env.ref('hr_timesheet.timesheet_action_project').id, _('See timesheet entries'), {'active_ids': projects_with_timesheets.ids}) - def action_show_timesheets_by_employee_invoice_type(self): - action = self.env["ir.actions.actions"]._for_xml_id("hr_timesheet.timesheet_action_all") - #Let's put the chart view first - new_views = [] - for view in action['views']: - new_views.insert(0, view) if view[1] == 'graph' else new_views.append(view) - action.update({ - 'display_name': _("Timesheets"), - 'domain': [('project_id', '=', self.id)], - 'context': { - 'default_project_id': self.id, - 'search_default_groupby_employee': True, - 'search_default_groupby_timesheet_invoice_type': True - }, - 'views': new_views - }) - - return action - def _convert_project_uom_to_timesheet_encode_uom(self, time): uom_from = self.company_id.project_time_mode_id uom_to = self.env.company.timesheet_encode_uom_id diff --git a/addons/hr_timesheet/views/project_portal_templates.xml b/addons/hr_timesheet/views/project_portal_templates.xml index 8a962f412d7..8e224c550ae 100644 --- a/addons/hr_timesheet/views/project_portal_templates.xml +++ b/addons/hr_timesheet/views/project_portal_templates.xml @@ -49,13 +49,13 @@ - 8 + 9 - + Days Spent Hours Spent - + diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 3114143c394..0e2e06c9d64 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -81,21 +81,6 @@ 24 - - - diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index dd1fdad5989..ed881247ff4 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -48,7 +48,6 @@ 'data/mail_template_data.xml', 'data/project_data.xml', 'wizard/project_task_type_delete_views.xml', - 'wizard/project_milestone_reach_wizard.xml', ], 'demo': [ 'data/mail_template_demo.xml', @@ -73,7 +72,6 @@ 'project/static/src/js/project_rating_graph_view.js', 'project/static/src/js/project_rating_pivot_view.js', 'project/static/src/js/project_task_kanban_examples.js', - 'project/static/src/js/subtasks_list_renderer.js', 'project/static/src/js/tours/project.js', 'project/static/src/js/project_calendar.js', 'project/static/src/js/widgets/*', diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index a4c3313dad7..33e0da030df 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -45,6 +45,10 @@ class ProjectCustomerPortal(CustomerPortal): project=project, task_url=f'projects/{project.id}/task', ) + # default value is set to 'project' in _prepare_tasks_values, so we have to set it to 'none' here. + if not groupby: + values['groupby'] = 'none' + return self._get_page_view_values(project, access_token, values, 'my_projects_history', False, **kwargs) def _prepare_project_domain(self): @@ -246,28 +250,33 @@ class ProjectCustomerPortal(CustomerPortal): } return self._get_page_view_values(task, access_token, values, history, False, **kwargs) - def _task_get_searchbar_sortings(self): - return { + def _task_get_searchbar_sortings(self, milestones_allowed): + values = { 'date': {'label': _('Newest'), 'order': 'create_date desc', 'sequence': 1}, 'name': {'label': _('Title'), 'order': 'name', 'sequence': 2}, 'project': {'label': _('Project'), 'order': 'project_id, stage_id', 'sequence': 3}, 'users': {'label': _('Assignees'), 'order': 'user_ids', 'sequence': 4}, 'stage': {'label': _('Stage'), 'order': 'stage_id, project_id', 'sequence': 5}, 'status': {'label': _('Status'), 'order': 'kanban_state', 'sequence': 6}, - 'priority': {'label': _('Priority'), 'order': 'priority desc', 'sequence': 7}, - 'date_deadline': {'label': _('Deadline'), 'order': 'date_deadline asc', 'sequence': 8}, - 'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc', 'sequence': 10}, + 'priority': {'label': _('Priority'), 'order': 'priority desc', 'sequence': 8}, + 'date_deadline': {'label': _('Deadline'), 'order': 'date_deadline asc', 'sequence': 9}, + 'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc', 'sequence': 11}, } + if milestones_allowed: + values['milestone'] = {'label': _('Milestone'), 'order': 'milestone_id', 'sequence': 7} + return values - def _task_get_searchbar_groupby(self): + def _task_get_searchbar_groupby(self, milestones_allowed): values = { 'none': {'input': 'none', 'label': _('None'), 'order': 1}, 'project': {'input': 'project', 'label': _('Project'), 'order': 2}, 'stage': {'input': 'stage', 'label': _('Stage'), 'order': 4}, 'status': {'input': 'status', 'label': _('Status'), 'order': 5}, - 'priority': {'input': 'priority', 'label': _('Priority'), 'order': 6}, - 'customer': {'input': 'customer', 'label': _('Customer'), 'order': 9}, + 'priority': {'input': 'priority', 'label': _('Priority'), 'order': 7}, + 'customer': {'input': 'customer', 'label': _('Customer'), 'order': 10}, } + if milestones_allowed: + values['milestone'] = {'input': 'milestone', 'label': _('Milestone'), 'order': 6} return dict(sorted(values.items(), key=lambda item: item[1]["order"])) def _task_get_groupby_mapping(self): @@ -275,6 +284,7 @@ class ProjectCustomerPortal(CustomerPortal): 'project': 'project_id', 'stage': 'stage_id', 'customer': 'partner_id', + 'milestone': 'milestone_id', 'priority': 'priority', 'status': 'kanban_state', } @@ -286,7 +296,7 @@ class ProjectCustomerPortal(CustomerPortal): return order return '%s, %s' % (field_name, order) - def _task_get_searchbar_inputs(self): + def _task_get_searchbar_inputs(self, milestones_allowed): values = { 'all': {'input': 'all', 'label': _('Search in All'), 'order': 1}, 'content': {'input': 'content', 'label': Markup(_('Search (in Content)')), 'order': 1}, @@ -295,9 +305,12 @@ class ProjectCustomerPortal(CustomerPortal): 'users': {'input': 'users', 'label': _('Search in Assignees'), 'order': 3}, 'stage': {'input': 'stage', 'label': _('Search in Stages'), 'order': 4}, 'status': {'input': 'status', 'label': _('Search in Status'), 'order': 5}, - 'priority': {'input': 'priority', 'label': _('Search in Priority'), 'order': 6}, - 'message': {'input': 'message', 'label': _('Search in Messages'), 'order': 10}, + 'priority': {'input': 'priority', 'label': _('Search in Priority'), 'order': 7}, + 'message': {'input': 'message', 'label': _('Search in Messages'), 'order': 11}, } + if milestones_allowed: + values['milestone'] = {'input': 'milestone', 'label': _('Search in Milestone'), 'order': 6} + return dict(sorted(values.items(), key=lambda item: item[1]["order"])) def _task_get_search_domain(self, search_in, search): @@ -315,6 +328,8 @@ class ProjectCustomerPortal(CustomerPortal): search_domain.append([('project_id', 'ilike', search)]) if search_in in ('ref', 'all'): search_domain.append([('id', 'ilike', search)]) + if search_in in ('milestone', 'all'): + search_domain.append([('milestone_id', 'ilike', search)]) if search_in in ('users', 'all'): user_ids = request.env['res.users'].sudo().search([('name', 'ilike', search)]) search_domain.append([('user_ids', 'in', user_ids.ids)]) @@ -328,12 +343,15 @@ class ProjectCustomerPortal(CustomerPortal): def _prepare_tasks_values(self, page, date_begin, date_end, sortby, search, search_in, groupby, url="/my/tasks", domain=None, su=False): values = self._prepare_portal_layout_values() - searchbar_sortings = dict(sorted(self._task_get_searchbar_sortings().items(), - key=lambda item: item[1]["sequence"])) - searchbar_inputs = self._task_get_searchbar_inputs() - searchbar_groupby = self._task_get_searchbar_groupby() Task = request.env['project.task'] + milestone_domain = AND([domain, [('allow_milestones', '=', 'True')]]) + milestones_allowed = Task.sudo().search_count(milestone_domain, limit=1) == 1 + searchbar_sortings = dict(sorted(self._task_get_searchbar_sortings(milestones_allowed).items(), + key=lambda item: item[1]["sequence"])) + searchbar_inputs = self._task_get_searchbar_inputs(milestones_allowed) + searchbar_groupby = self._task_get_searchbar_groupby(milestones_allowed) + if not domain: domain = [] if not su and Task.check_access_rights('read'): @@ -341,17 +359,20 @@ class ProjectCustomerPortal(CustomerPortal): Task = Task.sudo() # default sort by value - if not sortby: + if not sortby or (sortby == 'milestone' and not milestones_allowed): sortby = 'date' order = searchbar_sortings[sortby]['order'] # default group by value - if not groupby: + if not groupby or (groupby == 'milestone' and not milestones_allowed): groupby = 'project' if date_begin and date_end: domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)] + # search reset if needed + if not milestones_allowed and search_in == 'milestone': + search_in = 'all' # search if search and search_in: domain += self._task_get_search_domain(search_in, search) @@ -363,10 +384,25 @@ class ProjectCustomerPortal(CustomerPortal): tasks = Task.search(domain, order=order, limit=self._items_per_page, offset=pager_offset) request.session['my_project_tasks_history' if url.startswith('/my/projects') else 'my_tasks_history'] = tasks.ids[:100] + tasks_project_allow_milestone = tasks.filtered(lambda t: t.allow_milestones) + tasks_no_milestone = tasks - tasks_project_allow_milestone + groupby_mapping = self._task_get_groupby_mapping() group = groupby_mapping.get(groupby) if group: - grouped_tasks = [Task.concat(*g) for k, g in groupbyelem(tasks, itemgetter(group))] + if group == 'milestone_id': + grouped_tasks = [Task.concat(*g) for k, g in groupbyelem(tasks_project_allow_milestone, itemgetter(group))] + + if not grouped_tasks: + grouped_tasks = [tasks_no_milestone] + else: + if grouped_tasks[len(grouped_tasks) - 1][0].milestone_id and tasks_no_milestone: + grouped_tasks.append(tasks_no_milestone) + else: + grouped_tasks[len(grouped_tasks) - 1] |= tasks_no_milestone + + else: + grouped_tasks = [Task.concat(*g) for k, g in groupbyelem(tasks, itemgetter(group))] else: grouped_tasks = [tasks] @@ -382,6 +418,7 @@ class ProjectCustomerPortal(CustomerPortal): 'date': date_begin, 'date_end': date_end, 'grouped_tasks': get_grouped_tasks, + 'allow_milestone': milestones_allowed, 'page_name': 'task', 'default_url': url, 'task_url': 'tasks', diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 3ee25339c42..b50fc7ed2aa 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -778,21 +778,6 @@ class Project(models.Model): action_context['search_default_project_id'] = self.id return dict(action, context=action_context) - def action_view_analytic_account_entries(self): - self.ensure_one() - return { - 'res_model': 'account.analytic.line', - 'type': 'ir.actions.act_window', - 'name': _("Gross Margin"), - 'domain': [('account_id', '=', self.analytic_account_id.id)], - 'views': [(self.env.ref('analytic.view_account_analytic_line_tree').id, 'list'), - (self.env.ref('analytic.view_account_analytic_line_form').id, 'form'), - (self.env.ref('analytic.view_account_analytic_line_graph').id, 'graph'), - (self.env.ref('analytic.view_account_analytic_line_pivot').id, 'pivot')], - 'view_mode': 'tree,form,graph,pivot', - 'context': {'search_default_group_date': 1, 'default_account_id': self.analytic_account_id.id} - } - def action_get_list_view(self): self.ensure_one() return { @@ -899,7 +884,7 @@ class Project(models.Model): 'show': True, 'sequence': 3, }] - if self.user_has_groups('project.group_project_rating'): + if self.rating_count != 0 and self.user_has_groups('project.group_project_rating'): if self.rating_avg >= rating_data.RATING_AVG_TOP: icon = 'smile-o text-success' elif self.rating_avg >= rating_data.RATING_AVG_OK: @@ -2422,22 +2407,6 @@ class Task(models.Model): return children return children + children._get_all_subtasks(depth - 1) - def get_milestone_to_mark_as_reached_action(self): - """ Return an action if the milestone can be marked as reached otherwise return False """ - milestones = self.milestone_id.filtered('can_be_marked_as_done') - if milestones: - wizard = self.env['project.milestone.reach.wizard'].create({'line_ids': [Command.create({'milestone_id': m.id}) for m in milestones]}) - return { - 'name': _('Mark milestone as reached'), - 'view_mode': 'form', - 'res_model': 'project.milestone.reach.wizard', - 'views': [(self.env.ref('project.project_milestone_reach_wizard_view_form').id, 'form')], - 'type': 'ir.actions.act_window', - 'res_id': wizard.id, - 'target': 'new', - } - return False - def action_open_parent_task(self): return { 'name': _('Parent Task'), diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 08f048ceb24..3a32afac4a8 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -42,5 +42,3 @@ access_project_collaborator_user,project.collaborator.user,model_project_collabo access_project_collaborator_portal,project.collaborator.portal,model_project_collaborator,base.group_portal,1,0,0,0 access_project_share_manager,project.share.wizard.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0 access_project_personal_stage,project.personal.stage.user,model_project_task_stage_personal,base.group_user,1,1,1,1 -access_project_milestone_reach_wizard_project_user,project.milestone.reach.wizard.project.user,model_project_milestone_reach_wizard,project.group_project_user,1,1,1,0 -access_project_milestone_reach_line_wizard_project_user,project.milestone.reach.line.wizard.project.user,model_project_milestone_reach_line_wizard,project.group_project_user,1,1,1,0 diff --git a/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js b/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js index 4737090703b..50f41722f68 100644 --- a/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js +++ b/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { useService } from '@web/core/utils/hooks'; -import { formatFloat, formatMonetary } from '@web/views/fields/formatters'; +import { formatFloat } from '@web/views/fields/formatters'; import { session } from '@web/session'; import { ViewButton } from '@web/views/view_button/view_button'; import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog'; @@ -70,9 +70,9 @@ export class ProjectRightSidePanel extends Component { } formatMonetary(value, options = {}) { - const valueFormatted = formatMonetary(value, { - currencyId: this.currencyId, + const valueFormatted = formatFloat(value, { ...options, + 'digits': [false, 0], 'noSymbol': true, }); const currency = session.currencies[this.currencyId]; diff --git a/addons/project/static/src/components/subtask_one2many_field/subtask_list_renderer.js b/addons/project/static/src/components/subtask_one2many_field/subtask_list_renderer.js deleted file mode 100644 index 519a008693a..00000000000 --- a/addons/project/static/src/components/subtask_one2many_field/subtask_list_renderer.js +++ /dev/null @@ -1,20 +0,0 @@ -/** @odoo-module */ - -import { useService } from "@web/core/utils/hooks"; -import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; -import { ListRenderer } from '@web/views/list/list_renderer'; - -export class SubtaskListRenderer extends ListRenderer { - setup() { - super.setup(); - this.dialog = useService("dialog"); - } - - async onDeleteRecord(record) { - this.dialog.add(ConfirmationDialog, { - body: this.env._t("Are you sure you want to delete this record?"), - confirm: () => super.onDeleteRecord(record), - cancel: () => {}, - }); - } -} diff --git a/addons/project/static/src/components/subtask_one2many_field/subtask_one2many_field.js b/addons/project/static/src/components/subtask_one2many_field/subtask_one2many_field.js deleted file mode 100644 index 5a60b32be68..00000000000 --- a/addons/project/static/src/components/subtask_one2many_field/subtask_one2many_field.js +++ /dev/null @@ -1,19 +0,0 @@ -/** @odoo-module */ - -import { registry } from "@web/core/registry"; -import { X2ManyField } from '@web/views/fields/x2many/x2many_field'; - -import { SubtaskListRenderer } from './subtask_list_renderer'; - -export class SubtaskOne2ManyField extends X2ManyField { - get Renderer() { - return this.viewMode === 'list' ? SubtaskListRenderer : super.Renderer; - } -} - -SubtaskOne2ManyField.components = { - ...X2ManyField, - ListRenderer: SubtaskListRenderer, -} - -registry.category("fields").add("subtasks_one2many", SubtaskOne2ManyField); diff --git a/addons/project/static/src/js/project_form.js b/addons/project/static/src/js/project_form.js index 692ffa768d4..6dc7c270c83 100644 --- a/addons/project/static/src/js/project_form.js +++ b/addons/project/static/src/js/project_form.js @@ -111,25 +111,6 @@ const ProjectFormController = FormController.extend({ dialog.open(); }, - async _applyChanges(dataPointID, changes, event) { - const result = await this._super(...arguments); - if (event.data.force_save && 'stage_id' in changes) { - this._getMilestoneReachWizardAction([parseInt(event.target.res_id)]); - } - return result; - }, - async _getMilestoneReachWizardAction(recordIds) { - const action = await this._rpc({ - model: 'project.task', - method: 'get_milestone_to_mark_as_reached_action', - args: [recordIds], - }); - if (action) { - this.trigger_up('do-action', { - action, - }); - } - }, async _saveRecord(recordID, options) { const task = this.model.get(recordID || this.handle); const result = await this._super(...arguments); diff --git a/addons/project/static/src/js/subtasks_list_renderer.js b/addons/project/static/src/js/subtasks_list_renderer.js deleted file mode 100644 index 5eab262da0f..00000000000 --- a/addons/project/static/src/js/subtasks_list_renderer.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @odoo-module **/ - -import ListRenderer from 'web.ListRenderer'; -import Dialog from 'web.Dialog'; -import { _t } from 'web.core'; - -export const SubTasksListRenderer = ListRenderer.extend({ - events: Object.assign({}, ListRenderer.prototype.events, { - 'click tr .o_list_record_remove': '_onClickOpenDialog', - }), - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Confirmation dialog is opened when deleted any record from list. - * - * @private - * @param {MouseEvent} ev - */ - _onClickOpenDialog(ev) { - ev.stopPropagation(); - Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), { - confirm_callback: () => { - this._onRemoveIconClick(ev); - }, - cancel_callback: () => { - return false; - }, - }); - }, - -}); diff --git a/addons/project/static/src/js/widgets/subtasks_one2many_widget.js b/addons/project/static/src/js/widgets/subtasks_one2many_widget.js deleted file mode 100644 index 3a23db8f3ec..00000000000 --- a/addons/project/static/src/js/widgets/subtasks_one2many_widget.js +++ /dev/null @@ -1,21 +0,0 @@ -/** @odoo-module **/ - -import { FieldOne2Many } from 'web.relational_fields'; -import fieldRegistry from 'web.field_registry'; -import { SubTasksListRenderer } from '../subtasks_list_renderer'; - -const SubTasksFieldOne2Many = FieldOne2Many.extend({ - /** - * We want to use our custom renderer for the list. - * - * @override - */ - _getRenderer() { - if (this.view.arch.tag === 'tree') { - return SubTasksListRenderer; - } - return this._super.apply(...arguments); - }, -}); - -fieldRegistry.add('subtasks_one2many', SubTasksFieldOne2Many); diff --git a/addons/project/tests/test_project_recurrence.py b/addons/project/tests/test_project_recurrence.py index 7f3d68e86ae..4508545ec3f 100644 --- a/addons/project/tests/test_project_recurrence.py +++ b/addons/project/tests/test_project_recurrence.py @@ -472,12 +472,13 @@ class TestProjectrecurrence(TransactionCase): 'name': 'Parent Task', 'project_id': self.project_recurring.id }) + child_task = self.env['project.task'].create({ + 'name': 'Child Task', + 'parent_id': parent_task.id, + }) domain = [('project_id', '=', self.project_recurring.id)] - with Form(parent_task.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Test Subtask 1' with freeze_time("2020-01-01"): - with Form(parent_task.child_ids.with_context({'tracking_disable': True})) as form: + with Form(child_task.with_context({'tracking_disable': True})) as form: form.description = 'my super recurring task bla bla bla' form.date_deadline = datetime(2020, 2, 1) form.display_project_id = parent_task.project_id @@ -569,12 +570,20 @@ class TestProjectrecurrence(TransactionCase): 'project_id': self.project_recurring.id }) domain = [('project_id', '=', self.project_recurring.id)] + child_task_1, child_task_2_recurrence = self.env['project.task'].create([ + {'name': 'Child task 1'}, + {'name': 'Child task 2 that have recurrence'}, + ]) with Form(parent_task.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Child task 1' - with Form(parent_task.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Child task 2 that have recurrence' + task_form.child_ids.add(child_task_1) + task_form.child_ids.add(child_task_2_recurrence) + + grand_child_task_1 = self.env['project.task'].create({ + 'name': 'Grandchild task 1 (recurrent)', + }) + grand_child_task_2_recurrence = self.env['project.task'].create({ + 'name': 'Grandchild task 2', + }) with freeze_time("2020-01-01"): recurrent_subtask = parent_task.child_ids[0] with Form(recurrent_subtask.with_context(tracking_disable=True)) as task_form: @@ -586,11 +595,8 @@ class TestProjectrecurrence(TransactionCase): task_form.repeat_on_month = 'date' task_form.repeat_day = '15' task_form.date_deadline = datetime(2020, 2, 1) - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Grandchild task 1 (recurrent)' - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Grandchild task 2' - + task_form.child_ids.add(grand_child_task_1) + task_form.child_ids.add(grand_child_task_2_recurrence) # configure recurring subtask recurrent_subsubtask = recurrent_subtask.child_ids.filtered(lambda t: t.name == 'Grandchild task 1 (recurrent)') non_recurrent_subsubtask = recurrent_subtask.child_ids.filtered(lambda t: t.name == 'Grandchild task 2') @@ -602,18 +608,20 @@ class TestProjectrecurrence(TransactionCase): subtask_form.repeat_number = 4 subtask_form.date_deadline = datetime(2020, 2, 3) + grand_child_task_3, grand_child_task_4, grand_child_task_5 = self.env['project.task'].create([ + {'name': 'Grandchild task 3'}, + {'name': 'Grandchild task 4'}, + {'name': 'Grandchild task 5'}, + ]) # create non-recurring grandchild subtasks with Form(non_recurrent_subsubtask.with_context(tracking_disable=True)) as subtask_form: - with subtask_form.child_ids.new() as subsubtask_form: - subsubtask_form.name = 'Grandchild task 3' + subtask_form.child_ids.add(grand_child_task_3) non_recurrent_subsubtask = non_recurrent_subsubtask.child_ids with Form(non_recurrent_subsubtask.with_context(tracking_disable=True)) as subtask_form: - with subtask_form.child_ids.new() as subsubtask_form: - subsubtask_form.name = 'Grandchild task 4' + subtask_form.child_ids.add(grand_child_task_4) non_recurrent_subsubtask = non_recurrent_subsubtask.child_ids with Form(non_recurrent_subsubtask.with_context(tracking_disable=True)) as subtask_form: - with subtask_form.child_ids.new() as subsubtask_form: - subsubtask_form.name = 'Grandchild task 5' + subtask_form.child_ids.add(grand_child_task_5) self.assertTrue(recurrent_subtask.recurrence_id) self.assertEqual(recurrent_subtask.recurrence_id.next_recurrence_date, date(2020, 1, 15)) diff --git a/addons/project/tests/test_project_subtasks.py b/addons/project/tests/test_project_subtasks.py index 66f134417fa..109a2c5fc9d 100644 --- a/addons/project/tests/test_project_subtasks.py +++ b/addons/project/tests/test_project_subtasks.py @@ -108,26 +108,25 @@ class TestProjectSubtasks(TestProjectCommon): - The display project id should follow the project id """ # 1) + test_subtask_1 = self.env['project.task'].create({ + 'name': 'Test Subtask 1', + }) with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Test Subtask 1' + task_form.child_ids.add(test_subtask_1) self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "The project should be assigned from the default project.") self.assertFalse(self.task_1.child_ids.display_project_id, "The display project of a sub task should be false to project_id.") # 2) with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.edit(0) as subtask_form: - subtask_form.display_project_id = self.project_goats - + task_form.child_ids[0].display_project_id = self.project_goats self.assertEqual(self.task_1.project_id, self.project_pigs, "Changing the project of a subtask should not change parent project") self.assertEqual(self.task_1.child_ids.display_project_id, self.project_goats, "Display Project of the task should be well assigned") self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Changing display project id on a subtask should change project id") # 3) with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.edit(0) as subtask_form: - subtask_form.display_project_id = self.env['project.project'] + task_form.child_ids[0].display_project_id = self.env['project.project'] self.assertFalse(self.task_1.child_ids.display_project_id, "Display Project of the task should be well assigned, to False") self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "Resetting display project to False on a subtask should change project id to parent project id") @@ -142,8 +141,7 @@ class TestProjectSubtasks(TestProjectCommon): # 5) with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.edit(0) as subtask_form: - subtask_form.display_project_id = self.project_goats + task_form.child_ids[0].display_project_id = self.project_goats task_form.project_id = self.project_pigs self.assertEqual(self.task_1.project_id, self.project_pigs, "Parent project should change back.") @@ -162,10 +160,12 @@ class TestProjectSubtasks(TestProjectCommon): self.assertFalse(orphan_subtask.parent_id, "Parent should be false") # 7) + test_subtask_1 = self.env['project.task'].create({ + 'name': 'Test Subtask 1', + }) with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Test Subtask 1' - subtask_form.display_project_id = self.project_goats + task_form.child_ids.add(test_subtask_1) + task_form.child_ids[0].display_project_id = self.project_goats with Form(self.task_1.child_ids.with_context({'tracking_disable': True})) as subtask_form: subtask_form.display_project_id = self.env['project.project'] subtask_form.parent_id = self.env['project.task'] @@ -183,9 +183,11 @@ class TestProjectSubtasks(TestProjectCommon): self.project_pigs.type_ids |= stage_a self.project_pigs.type_ids |= stage_b + test_subtask_1 = self.env['project.task'].create({ + 'name': 'Test Subtask 1', + }) with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Test Subtask 1' + task_form.child_ids.add(test_subtask_1) self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project.") @@ -194,16 +196,17 @@ class TestProjectSubtasks(TestProjectCommon): self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should remain the same while changing parent task stage.") + test_subtask_2 = self.env['project.task'].create({ + 'name': 'Test Subtask 2', + }) with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - task_form.child_ids.remove(0) - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Test Subtask 2' + task_form.child_ids.remove(test_subtask_1.id) + task_form.child_ids.add(test_subtask_2) self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project even if parent stage id is different.") with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.edit(0) as subtask_form: - subtask_form.display_project_id = self.project_goats + task_form.child_ids[0].display_project_id = self.project_goats self.assertEqual(self.task_1.child_ids.stage_id.name, "New", "The stage of the child task should be the default one of the display project id, once set.") @@ -242,15 +245,24 @@ class TestProjectSubtasks(TestProjectCommon): 4) check the correct nb of sub-tasks is displayed in the 'sub-tasks' stat button and on the parent task kanban card 5) sub-tasks should be copied when the parent task is duplicated """ - with Form(self.task_1.with_context({'tracking_disable': True})) as task_form: - with task_form.child_ids.new() as subtask_form: - subtask_form.name = 'Test Subtask 1' - subtask_form.display_project_id = self.env['project.project'] - child_subtask = self.task_1.child_ids[0] - with Form(child_subtask.with_context(tracking_disable=True)) as subtask_form: - with subtask_form.child_ids.new() as subsubtask_form: - subsubtask_form.name = 'Test Subtask/Subtask' + test_subtask_1 = self.env['project.task'].create({ + 'name': 'Test Subtask 1', + }) + + task_form = Form(self.task_1.with_context({'tracking_disable': True})) + task_form.child_ids.add(test_subtask_1) + task_form.child_ids[0].display_project_id = self.env['project.project'] task = task_form.save() + + child_subtask = self.task_1.child_ids[0] + test_subtask_2 = self.env['project.task'].create({ + 'name': 'Test Subtask 2', + }) + + with Form(child_subtask.with_context(tracking_disable=True)) as subtask_form: + subtask_form.child_ids.add(test_subtask_2) + subtask_form.child_ids[0].display_project_id = self.env['project.project'] + self.assertEqual(task.subtask_count, 2, "Parent task should have 2 children") task_2 = task.copy() self.assertEqual(task_2.subtask_count, 2, "If the parent task is duplicated then the sub task should be copied") diff --git a/addons/project/views/project_collaborator_views.xml b/addons/project/views/project_collaborator_views.xml index 15e26993ae4..34cb04bdd09 100644 --- a/addons/project/views/project_collaborator_views.xml +++ b/addons/project/views/project_collaborator_views.xml @@ -1,21 +1,6 @@ - - project.collaborator.view.form - project.collaborator - -
- - - - - - -
-
-
- project.collaborator.view.tree project.collaborator @@ -45,7 +30,7 @@ Project Collaborators project.collaborator - tree,form + tree [('project_id', '=', active_id)] diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml index 52eed546043..87519db6084 100644 --- a/addons/project/views/project_portal_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -110,13 +110,15 @@ - - + + + Name - Assignees + Assignees + Milestone Project Stage @@ -127,6 +129,12 @@ + + + No Milestone + @@ -149,13 +157,18 @@ - +
User
+ + + + + @@ -294,6 +307,7 @@
Project:
Deadline:
+
Milestone:
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index f039715c6b1..de48ef43c38 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -457,45 +457,6 @@
- - - - - - - - - - - - - - diff --git a/addons/project_mrp/__manifest__.py b/addons/project_mrp/__manifest__.py index 77752b2b48e..53d46b657dc 100644 --- a/addons/project_mrp/__manifest__.py +++ b/addons/project_mrp/__manifest__.py @@ -7,9 +7,6 @@ 'summary': "Monitor MRP using project", 'category': 'Services/Project', 'depends': ['mrp_account', 'project'], - 'data': [ - 'views/project_views.xml', - ], 'demo': [ 'data/project_mrp_demo.xml', ], diff --git a/addons/project_mrp/views/project_views.xml b/addons/project_mrp/views/project_views.xml deleted file mode 100644 index 71d479c2725..00000000000 --- a/addons/project_mrp/views/project_views.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - project.project.view.inherited - project.project - - - - - - - - - - - diff --git a/addons/project_purchase/__manifest__.py b/addons/project_purchase/__manifest__.py index 522ce80aae0..e0b4a13a6ca 100644 --- a/addons/project_purchase/__manifest__.py +++ b/addons/project_purchase/__manifest__.py @@ -7,9 +7,6 @@ 'summary': "Monitor purchase in project", 'category': 'Services/Project', 'depends': ['purchase', 'project'], - 'data': [ - 'views/project_views.xml', - ], 'demo': [ 'data/project_purchase_demo.xml', ], diff --git a/addons/project_purchase/views/project_views.xml b/addons/project_purchase/views/project_views.xml deleted file mode 100644 index 42d71d8ffa7..00000000000 --- a/addons/project_purchase/views/project_views.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - project.project.view.inherited - project.project - - 30 - - - - - - - diff --git a/addons/sale_project/controllers/portal.py b/addons/sale_project/controllers/portal.py index 2a6029e40bf..b621011cbdf 100644 --- a/addons/sale_project/controllers/portal.py +++ b/addons/sale_project/controllers/portal.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import _ +from odoo import _lt from odoo.osv.expression import OR from odoo.addons.project.controllers.portal import ProjectCustomerPortal @@ -9,10 +9,10 @@ from odoo.addons.project.controllers.portal import ProjectCustomerPortal class SaleProjectCustomerPortal(ProjectCustomerPortal): - def _task_get_searchbar_groupby(self): - values = super()._task_get_searchbar_groupby() - values['sale_order'] = {'input': 'sale_order', 'label': _('Sales Order'), 'order': 7} - values['sale_line'] = {'input': 'sale_line', 'label': _('Sales Order Item'), 'order': 8} + def _task_get_searchbar_groupby(self, milestones_allowed): + values = super()._task_get_searchbar_groupby(milestones_allowed) + values['sale_order'] = {'input': 'sale_order', 'label': _lt('Sales Order'), 'order': 8} + values['sale_line'] = {'input': 'sale_line', 'label': _lt('Sales Order Item'), 'order': 9} return dict(sorted(values.items(), key=lambda item: item[1]["order"])) def _task_get_groupby_mapping(self): @@ -20,11 +20,11 @@ class SaleProjectCustomerPortal(ProjectCustomerPortal): groupby_mapping.update(sale_order='sale_order_id', sale_line='sale_line_id') return groupby_mapping - def _task_get_searchbar_inputs(self): - values = super()._task_get_searchbar_inputs() - values['sale_order'] = {'input': 'sale_order', 'label': _('Search in Sales Order'), 'order': 7} - values['sale_line'] = {'input': 'sale_line', 'label': _('Search in Sales Order Item'), 'order': 8} - values['invoice'] = {'input': 'invoice', 'label': _('Search in Invoice'), 'order': 9} + def _task_get_searchbar_inputs(self, milestones_allowed): + values = super()._task_get_searchbar_inputs(milestones_allowed) + values['sale_order'] = {'input': 'sale_order', 'label': _lt('Search in Sales Order'), 'order': 8} + values['sale_line'] = {'input': 'sale_line', 'label': _lt('Search in Sales Order Item'), 'order': 9} + values['invoice'] = {'input': 'invoice', 'label': _lt('Search in Invoice'), 'order': 10} return dict(sorted(values.items(), key=lambda item: item[1]["order"])) def _task_get_search_domain(self, search_in, search): diff --git a/addons/sale_project/models/project_milestone.py b/addons/sale_project/models/project_milestone.py index 9e0afc0324a..76040c4828a 100644 --- a/addons/sale_project/models/project_milestone.py +++ b/addons/sale_project/models/project_milestone.py @@ -12,7 +12,7 @@ class ProjectMilestone(models.Model): sale_line_id = fields.Many2one('sale.order.line', 'Sales Order Item', help='Sales Order Item that will be updated once the milestone is reached.', domain="[('order_partner_id', '=?', project_partner_id), ('qty_delivered_method', '=', 'milestones')]") - quantity_percentage = fields.Float('Quantity (%)', help='Percentage of the ordered quantity that will automatically be delivered once the milestone is reached.') + quantity_percentage = fields.Float('Quantity', help='Percentage of the ordered quantity that will automatically be delivered once the milestone is reached.') sale_line_name = fields.Text(related='sale_line_id.name') diff --git a/addons/sale_project/models/sale_order_line.py b/addons/sale_project/models/sale_order_line.py index 16510b96af4..0af8ebb18d1 100644 --- a/addons/sale_project/models/sale_order_line.py +++ b/addons/sale_project/models/sale_order_line.py @@ -301,6 +301,18 @@ class SaleOrderLine(models.Model): project = map_so_project[so_line.order_id.id] if not so_line.task_id: so_line._timesheet_create_task(project=project) + so_line._generate_milestone() + + def _generate_milestone(self): + if self.product_id.service_policy == 'delivered_milestones': + milestone = self.env['project.milestone'].create({ + 'name': self.name, + 'project_id': self.project_id.id, + 'sale_line_id': self.id, + 'quantity_percentage': 1, + }) + if self.product_id.service_tracking == 'task_in_project': + self.task_id.milestone_id = milestone.id def _prepare_invoice_line(self, **optional_values): """ diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml index 3d1d0956f85..3534cda64fa 100644 --- a/addons/sale_project/views/project_task_views.xml +++ b/addons/sale_project/views/project_task_views.xml @@ -35,14 +35,6 @@ - {'always_reload': True} {'res_partner_search_mode': 'customer'} @@ -60,32 +52,6 @@
- - - - - - diff --git a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js index 518448675bc..508dfcc5d7d 100644 --- a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js +++ b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js @@ -193,7 +193,7 @@ tour.register('sale_timesheet_tour', { trigger: '.oe_kanban_global_click :contains("Project for Freeman") button.o_dropdown_kanban', content: 'Open the project dropdown', }, { - trigger: '.o_kanban_record:contains("Project for Freeman") .dropdown-menu a:contains("Edit")', + trigger: '.o_kanban_record:contains("Project for Freeman") .dropdown-menu a:contains("Settings")', content: 'Start editing the project', // timer: 300, }, { diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 2c4808156d1..6ed928ca836 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -6,23 +6,6 @@ project.project - - - @@ -31,8 +14,8 @@ - - + + diff --git a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js index d2f9c7ec4d2..fd8d9e01ae2 100644 --- a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js +++ b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js @@ -820,109 +820,55 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p content: _t('See Tasks'), position: 'bottom', }, { - trigger: '.o_field_widget[name=project_id] a', - content: _t('See Project'), - position: 'right', -}, -tour.stepUtils.autoExpandMoreButtons('.o_control_panel .breadcrumb:contains("the_flow.project")'), -{ - trigger: '.oe_button_box button.oe_stat_button:has(span:contains("Recorded"))', - extra_trigger: '.o_form_readonly .oe_stat_button.dropdown > .dropdown-menu', - content: _t('See Timesheets'), - position: 'bottom', + trigger: 'a.nav-link:contains(Timesheets)', + extra_trigger: 'div.o_notebook_headers', + content: 'Click on Timesheets page to log a timesheet', }, { - mobile: true, - trigger: '.o_enable_searchview', - content: _t('Open the search field'), - position: 'bottom', -}, { - mobile: true, - trigger: '.o_toggle_searchview_full', - content: _t('Open the full search field'), - position: 'bottom', -}, { - trigger: '.o_group_by_menu .dropdown-toggle', - content: _t('Click on the group by button'), - position: 'bottom', -}, { - trigger: '.dropdown-item.selected:contains(Employee)', - extra_trigger: '.dropdown.show', - content: _t('Remove group by employee'), - position: 'bottom', -}, { - trigger: '.dropdown-item.selected:contains(Billing Type)', - extra_trigger: '.dropdown.show', - content: _t('Remove group by Billing type'), - position: 'bottom', -}, { - mobile: true, - trigger: '.o_mobile_search_footer', - content: _t('Close the search dropdown on mobile'), - position: 'bottom', -}, { - mobile: true, - trigger: '.o_cp_bottom_right .o_cp_switch_buttons .btn-link', - content: _t('Prepare to change the view on mobile'), - position: 'bottom', -}, { - trigger: '.o_switch_view.o_list', - content: _t('Activate the list view'), - position: 'bottom', -}, { - trigger: '.o_list_button_add', - content: _t('Add a Timesheet'), - position: 'bottom', -}, { - // FIXME replace list by kanban + form - // FIXME WOWL: remove first part of selector when legacy view is dropped - // (currently, it's anew view in community and a legacy one in enterprise) - trigger: '.o_selected_row input[name=name], .o_selected_row .o_field_widget[name=name] input', - content: _t('Set description'), - position: 'bottom', - run: 'text 10 hours', -}, { - trigger: '.o_selected_row .o_field_widget[name=task_id] input', - content: _t('Choose a task'), - position: 'bottom', - run: 'click', + trigger: '.o_form_button_edit', + content: _t('Click on Edit button to enter to the form view of the task.'), }, { mobile: false, - trigger: ".ui-menu-item > a", - auto: true, + trigger: 'div[name="timesheet_ids"] td.o_field_x2many_list_row_add a[role="button"]', + content: 'Click on Add a line to create a new timesheet into the task.', }, { mobile: true, - trigger: ".o_kanban_record:has(.o_kanban_record_title :contains('the_flow.service'))", - extra_trigger: ".modal:not(.o_inactive_modal) .modal-title:contains('Task')", - content: _t("Select the the_flow.vendor"), - position: "bottom", -}, { - edition: 'community', - trigger: '.o_selected_row .o_field_widget[name=unit_amount] input', - content: _t('Set time'), + trigger: '.o-kanban-button-new', + content: _t("Click on Add in order to add a timesheet."), position: 'bottom', +}, { + mobile: false, + trigger: 'div[name="timesheet_ids"] div[name="name"] input', + content: 'Enter a description this timesheet', + run: 'text 10 hours', +}, { + mobile: true, + trigger: '.modal-body .o_form_view div[name="name"] input', + content: 'Enter a description this timesheet', + run: 'text 10 hours', +}, { + mobile: false, + trigger: 'div[name="timesheet_ids"] div[name="unit_amount"] input', + content: 'Enter one hour for this timesheet', run: 'text 10', }, { - edition: 'enterprise', - trigger: '.o_selected_row .o_field_widget[name=unit_amount]input', - content: _t('Set time'), - position: 'bottom', + mobile: true, + trigger: '.modal-body .o_form_view div[name="unit_amount"] input', + content: 'Enter one hour for this timesheet', run: 'text 10', }, { - trigger: '.o_list_button_save', + trigger: '.o_form_button_save', content: _t('Save'), position: 'bottom', }, ...tour.stepUtils.goBackBreadcrumbsMobile( _t('Back to the sale order'), undefined, - ".breadcrumb-item.active:contains('Timesheets')", - ".breadcrumb-item.active:contains('the_flow.project')", ".breadcrumb-item.active:contains('the_flow.service')" ), { mobile: false, trigger: '.breadcrumb-item:nth-child(2) a', - extra_trigger: '.o_list_button_add', // Waiting save + extra_trigger: 'div:not(".o_form_editable")', // Waiting save content: _t('Back to the sale order'), position: 'bottom', },