[IMP] project,*: improve user experience
This commit (and its enterprise equivalent) purpose is to smooth the user experience and remove unnecessary steps. The commits: - Remove all stat buttons except the status and collaborators one from the project edit form. - Add multiple small changes to string/name fo fields in views - Change the custom O2M widget for a standard M2M for the management of child tasks - Add the milestone field to the portal page of shared project. - Remove the wizard 'marked_as_reached_milestone' - Add the automatic generation of milestone when a SO is confirmed if some SOL need it task-2941835 closes odoo/odoo#98546 Related: odoo/enterprise#30628 Related: odoo/upgrade#3798 Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -49,13 +49,13 @@
|
||||
<t t-set="is_uom_day" t-value="timesheet_ids._is_timesheet_encode_uom_day()"/>
|
||||
</xpath>
|
||||
<xpath expr="//thead/tr/t[@t-set='number_of_header']" position="attributes">
|
||||
<attribute name="t-value">8</attribute>
|
||||
<attribute name="t-value">9</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//thead/tr/th[@name='project_portal_assignees']" position="after">
|
||||
<xpath expr="//thead/tr/th[@name='project_portal_milestones']" position="after">
|
||||
<th t-if="is_uom_day" class="text-end">Days Spent</th>
|
||||
<th t-else="" class="text-end">Hours Spent</th>
|
||||
</xpath>
|
||||
<xpath expr="//tbody/t/tr/td[@name='project_portal_assignees']" position="after">
|
||||
<xpath expr="//tbody/t/tr/td[@name='project_portal_milestones']" position="after">
|
||||
<td class="text-end">
|
||||
<t t-if="is_uom_day">
|
||||
<t t-out="timesheet_ids._convert_hours_to_days(task.effective_hours)"/>
|
||||
|
||||
@@ -81,21 +81,6 @@
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="priority">24</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_get_list_view']" position="after">
|
||||
<button class="oe_stat_button" name="action_show_timesheets_by_employee_invoice_type" type="object" icon="fa-clock-o" attrs="{'invisible': [('allow_timesheets', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<div class="oe_inline">
|
||||
<span class="o_stat_value me-1">
|
||||
<field name="total_timesheet_time" widget="statinfo" nolabel="1"/>
|
||||
</span>
|
||||
<span class="o_stat_value">
|
||||
<field name="timesheet_encode_uom_id" class="o_stat_text" options="{'no_open' : True}"/>
|
||||
</span>
|
||||
</div>
|
||||
<span class="o_stat_text">Recorded</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='dates']" position="after">
|
||||
<field name="allocated_hours" widget="timesheet_uom_no_toggle" attrs="{'invisible': [('allow_timesheets', '=', False)]}"/>
|
||||
</xpath>
|
||||
|
||||
@@ -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/*',
|
||||
|
||||
@@ -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 <span class="nolabel"> (in Content)</span>')), '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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
+3
-3
@@ -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];
|
||||
|
||||
@@ -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: () => {},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_collaborator_view_form" model="ir.ui.view">
|
||||
<field name="name">project.collaborator.view.form</field>
|
||||
<field name="model">project.collaborator</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project Collaborator" create="0" edit="0">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="project_id" options="{'no_create': True}" />
|
||||
<field name="partner_id" options="{'no_create': True}" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_sharing_access_view_tree" model="ir.ui.view">
|
||||
<field name="name">project.collaborator.view.tree</field>
|
||||
<field name="model">project.collaborator</field>
|
||||
@@ -45,7 +30,7 @@
|
||||
<record id="project_collaborator_action" model="ir.actions.act_window">
|
||||
<field name="name">Project Collaborators</field>
|
||||
<field name="res_model">project.collaborator</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="domain">[('project_id', '=', active_id)]</field>
|
||||
<field name="search_view_id" ref="project_collaborator_view_search"/>
|
||||
<field name="help" type="html">
|
||||
|
||||
@@ -110,13 +110,15 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- Allows overrides in modules -->
|
||||
<t t-set="group_by_in_header_list" t-value="['priority', 'status', 'project', 'stage']"></t>
|
||||
<t t-set="number_of_header" t-value="7"></t>
|
||||
<t t-set="group_by_in_header_list" t-value="['priority', 'status', 'project', 'stage', 'milestone']"></t>
|
||||
<t t-set="number_of_header" t-value="8"></t>
|
||||
<!-- Computes the right colspan once and use it everywhere -->
|
||||
<t t-set="grouped_tasks_colspan" t-value="number_of_header - 1 if groupby in group_by_in_header_list else number_of_header"></t>
|
||||
<t t-set="grouped_tasks_colspan" t-value="grouped_tasks_colspan if allow_milestone else grouped_tasks_colspan - 1"></t>
|
||||
<th t-attf-colspan="{{2 if groupby != 'priority' else 1}}"/>
|
||||
<th>Name</th>
|
||||
<th name="project_portal_assignees">Assignees</th>
|
||||
<th>Assignees</th>
|
||||
<th t-if="groupby != 'milestone' and allow_milestone" name="project_portal_milestones">Milestone</th>
|
||||
<th t-if="groupby != 'status'"/>
|
||||
<th t-if="groupby != 'project'">Project</th>
|
||||
<th t-if="groupby != 'stage'" class="text-end">Stage</th>
|
||||
@@ -127,6 +129,12 @@
|
||||
<tr t-if="not groupby == 'none'" class="table-light">
|
||||
<th t-if="groupby == 'project'" t-attf-colspan="{{grouped_tasks_colspan}}">
|
||||
<span t-field="tasks[0].sudo().project_id.name"/></th>
|
||||
<th t-if="groupby == 'milestone'" t-attf-colspan="{{grouped_tasks_colspan}}">
|
||||
<span t-if="tasks[0].sudo().milestone_id and tasks[0].sudo().allow_milestones"
|
||||
class="text-truncate"
|
||||
t-field="tasks[0].sudo().milestone_id.name"/>
|
||||
<span t-else="">No Milestone</span>
|
||||
</th>
|
||||
<th t-if="groupby == 'stage'" t-attf-colspan="{{grouped_tasks_colspan}}">
|
||||
<span class="text-truncate" t-field="tasks[0].sudo().stage_id.name"/></th>
|
||||
<th t-if="groupby == 'priority'" t-attf-colspan="{{grouped_tasks_colspan}}">
|
||||
@@ -149,13 +157,18 @@
|
||||
<td>
|
||||
<a t-attf-href="/my/#{task_url}/#{task.id}?{{ keep_query() }}"><span t-field="task.name"/></a>
|
||||
</td>
|
||||
<td name="project_portal_assignees">
|
||||
<td>
|
||||
<t t-set="assignees" t-value="task.sudo().user_ids"/>
|
||||
<div t-if="assignees" class="row flex-nowrap ps-3">
|
||||
<img class="rounded-circle o_portal_contact_img me-2" t-attf-src="#{image_data_uri(assignees[:1].avatar_1024)}" alt="User" style="width: 20px; height: 20px;"/>
|
||||
<span t-out="'%s%s' % (assignees[:1].name, ' + %s others' % len(assignees[1:]) if len(assignees.user_ids) > 1 else '')" t-att-title="'\n'.join(assignees[1:].mapped('name'))"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-if="groupby != 'milestone' and allow_milestone" name="project_portal_milestones">
|
||||
<t t-if="task.milestone_id and task.allow_milestones">
|
||||
<span t-esc="task.milestone_id.name" />
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="groupby != 'status'" align="right">
|
||||
<t t-call="project.portal_my_tasks_state_widget_template">
|
||||
<t t-set="path" t-value="'tasks'"/>
|
||||
@@ -294,6 +307,7 @@
|
||||
<div t-if="project_accessible"><strong>Project:</strong> <a t-attf-href="/my/projects/#{task.project_id.id}" t-field="task.project_id"/></div>
|
||||
<div t-else=""><strong>Project:</strong> <a t-field="task.project_id"/></div>
|
||||
<div t-if="task.date_deadline"><strong>Deadline:</strong> <span t-field="task.date_deadline" t-options='{"widget": "date"}'/></div>
|
||||
<div t-if="task.milestone_id and task.allow_milestones"><strong>Milestone:</strong> <span t-field="task.milestone_id"/></div>
|
||||
<div name="portal_my_task_planned_hours">
|
||||
<t t-call="project.portal_my_task_planned_hours_template"/>
|
||||
</div>
|
||||
|
||||
@@ -457,45 +457,6 @@
|
||||
</header>
|
||||
<sheet string="Project">
|
||||
<div class="oe_button_box" name="button_box" groups="base.group_user">
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<button class="oe_stat_button" type="object" name="action_view_analytic_account_entries" icon="fa-usd" attrs="{'invisible': [('analytic_account_id', '=', False)]}" groups="analytic.group_analytic_accounting">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="analytic_account_balance" widget='monetary'/>
|
||||
</span>
|
||||
<span class="o_stat_text">Gross Margin</span>
|
||||
</div>
|
||||
</button>
|
||||
<button class="oe_stat_button" type="action"
|
||||
name="%(act_project_project_2_project_task_all)d" icon="fa-tasks">
|
||||
<field string="Tasks In Progress" name="task_count" widget="statinfo" options="{'label_field': 'label_tasks'}"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="action_get_list_view" type="object" icon="fa-check-square-o" attrs="{'invisible': [('allow_milestones', '=', False)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="milestone_count" nolabel="1"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Milestones
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<field name="rating_avg" invisible="1"/>
|
||||
<field name="rating_count" invisible="1"/>
|
||||
<button name="action_view_all_rating" type="object" attrs="{'invisible': ['|', ('rating_active', '=', False), ('rating_count', '=', 0)]}" class="oe_stat_button" groups="project.group_project_rating">
|
||||
<i class="fa fa-fw o_button_icon fa-smile-o text-success" attrs="{'invisible': [('rating_avg', '<', 3.66)]}" title="Satisfied"/>
|
||||
<i class="fa fa-fw o_button_icon fa-meh-o text-warning" attrs="{'invisible': ['|', ('rating_avg', '<', 2.33), ('rating_avg', '>=', 3.66)]}" title="Okay"/>
|
||||
<i class="fa fa-fw o_button_icon fa-frown-o text-danger" attrs="{'invisible': [('rating_avg', '>=', 2.33)]}" title="Dissatisfied"/>
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="rating_avg_percentage" nolabel="1" widget="percentage"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Satisfaction
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="project_update_all_action" type="object" groups="project.group_project_manager">
|
||||
<div class="ps-4">
|
||||
<field name="last_update_color" invisible="1"/>
|
||||
@@ -508,11 +469,6 @@
|
||||
<field name="last_update_status" readonly="1" widget="status_with_color" options="{'color_field': 'last_update_color'}"/>
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(action_project_task_burndown_chart_report)d" type="action" class="oe_stat_button" icon="fa-area-chart" groups="project.group_project_user">
|
||||
<span class="o_stat_text">
|
||||
Burndown Chart
|
||||
</span>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="%(project.project_collaborator_action)d" type="action" icon="fa-users" groups="project.group_project_manager" attrs="{'invisible':[('privacy_visibility', '!=', 'portal')]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
@@ -908,7 +864,7 @@
|
||||
</div>
|
||||
<div role="menuitem" class="col-6" groups="project.group_project_manager">
|
||||
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_wizard_action)d" type="action">Share</a>
|
||||
<a class="dropdown-item" role="menuitem" type="edit">Edit</a>
|
||||
<a class="dropdown-item" role="menuitem" type="edit">Settings</a>
|
||||
</div>
|
||||
<div class="o_kanban_card_manage_section o_kanban_manage_view col-12 row ps-0" groups="!project.group_project_manager">
|
||||
<div role="menuitem" class="w-100">
|
||||
@@ -1214,7 +1170,11 @@
|
||||
<field name="description" type="html" options="{'collaborative': true, 'resizable': false}"/>
|
||||
</page>
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
|
||||
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id, 'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id}" widget="subtasks_one2many">
|
||||
<field name="child_ids"
|
||||
context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id,
|
||||
'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id, 'search_default_display_project_id': project_id }"
|
||||
widget="many2many"
|
||||
domain="['!', ('id', 'parent_of', id)]">
|
||||
<tree editable="bottom">
|
||||
<field name="legend_normal" invisible="1"/>
|
||||
<field name="legend_done" invisible="1"/>
|
||||
@@ -1290,8 +1250,10 @@
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page name="recurrence" string="Recurrence" attrs="{'invisible': [('recurring_task', '=', False)]}" groups="project.group_project_recurring_tasks">
|
||||
<group>
|
||||
<page name="recurrence" string="Recurrent" groups="project.group_project_recurring_tasks">
|
||||
<span class="o_form_label"> Recurrent </span>
|
||||
<field name="recurring_task" attrs="{'invisible': ['|', ('allow_recurring_tasks', '=', False), ('active', '=', False)]}"/>
|
||||
<group attrs="{'invisible': [('recurring_task', '=', False)]}">
|
||||
<group>
|
||||
<label for="repeat_interval" />
|
||||
<div class="o_col">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import project_milestone_reach_wizard
|
||||
from . import project_task_type_delete
|
||||
from . import project_share_wizard
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class ProjectMilestoneReachWizard(models.TransientModel):
|
||||
_name = 'project.milestone.reach.wizard'
|
||||
_description = 'Mark one or more milestones as reached'
|
||||
|
||||
milestone_id = fields.Many2one(related='line_ids.milestone_id')
|
||||
line_ids = fields.One2many('project.milestone.reach.line.wizard', 'wizard_id')
|
||||
line_count = fields.Integer(compute='_compute_line_count')
|
||||
|
||||
@api.depends('line_ids')
|
||||
def _compute_line_count(self):
|
||||
for wizard in self:
|
||||
wizard.line_count = len(wizard.line_ids)
|
||||
|
||||
def action_mark_milestone_as_reach(self):
|
||||
milestones = self.line_ids.filtered('mark_as_reached').milestone_id if self.line_count > 1 else self.milestone_id
|
||||
if not milestones:
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
milestones.is_reached = True
|
||||
if len(milestones) == 1:
|
||||
message = _('The %s milestone has successfully been marked as reached.', milestones.display_name)
|
||||
else:
|
||||
message = _('The selected milestones have successfully been marked as reached.')
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': message,
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProjectMilestoneReachLineWizard(models.TransientModel):
|
||||
_name = 'project.milestone.reach.line.wizard'
|
||||
_description = 'Mark a milestone as reached'
|
||||
|
||||
wizard_id = fields.Many2one('project.milestone.reach.wizard', readonly=True)
|
||||
milestone_id = fields.Many2one('project.milestone', readonly=True)
|
||||
mark_as_reached = fields.Boolean(default=True)
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_milestone_reach_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">project.milestone.reach.wizard.view.form</field>
|
||||
<field name="model">project.milestone.reach.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="line_count" invisible="1" />
|
||||
<span attrs="{'invisible': [('line_count', '!=', 1)]}">Congratulations, all of the tasks linked to the <field name="milestone_id" nolabel="1" /> milestone are now done. Would you like to mark this milestone as reached?</span>
|
||||
<span attrs="{'invisible': [('line_count', '=', 1)]}">Congratulations, all of the tasks linked to those milestones are now done. Would you like to mark those milestones as reached?</span>
|
||||
<group attrs="{'invisible': [('line_count', '=', 1)]}">
|
||||
<field name="line_ids" nolabel="1">
|
||||
<tree create="0" editable="1">
|
||||
<field name="milestone_id" />
|
||||
<field name="mark_as_reached" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Mark As Reached" type="object" name="action_mark_milestone_as_reach" class="btn btn-primary" data-hotkey="q"/>
|
||||
<button string="Discard" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -9,9 +9,6 @@
|
||||
'summary': 'Project expenses',
|
||||
'description': 'Bridge created to add the number of expenses linked to an AA to a project form',
|
||||
'depends': ['project', 'hr_expense'],
|
||||
'data': [
|
||||
'views/project_project_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/project_hr_expense_demo.xml',
|
||||
],
|
||||
|
||||
@@ -44,14 +44,6 @@ class Project(models.Model):
|
||||
action["res_id"] = expense_ids[0]
|
||||
return action
|
||||
|
||||
def action_open_project_expenses(self):
|
||||
if not self.analytic_account_id:
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
expense_ids = self.env['hr.expense']._search([
|
||||
('analytic_account_id', 'in', self.analytic_account_id.ids)
|
||||
])
|
||||
return self._get_expense_action(expense_ids=expense_ids)
|
||||
|
||||
def action_profitability_items(self, section_name, domain=None, res_id=False):
|
||||
if section_name == 'expenses':
|
||||
return self._get_expense_action(domain, [res_id] if res_id else [])
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_project_view_form" model="ir.ui.view">
|
||||
<field name="name">project.project.form.inherit</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="priority">40</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_analytic_account_entries']" position="after">
|
||||
<button name="action_open_project_expenses" type="object" class="oe_stat_button" icon="fa-money" groups="hr_expense.group_hr_expense_team_approver">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="expenses_count" nolabel="1"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Expenses
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="project_project_form_view_inherited" model="ir.ui.view">
|
||||
<field name="name">project.project.view.inherited</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project" />
|
||||
<field eval="20" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_analytic_account_entries']" position="after">
|
||||
<button class="oe_stat_button" type="object" name="action_view_mrp_production"
|
||||
icon="fa-wrench"
|
||||
groups="mrp.group_mrp_user">
|
||||
<field string="Manufacturing Orders" name="production_count" widget="statinfo"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" type="object" name="action_view_workorder"
|
||||
icon="fa-cog" attrs="{'invisible': [('workorder_count', '=', 0)]}"
|
||||
groups="mrp.group_mrp_user">
|
||||
<field string="Work Orders" name="workorder_count" widget="statinfo"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" type="object" name="action_view_mrp_bom"
|
||||
icon="fa-flask"
|
||||
groups="mrp.group_mrp_user">
|
||||
<field string="Bills of Materials" name="bom_count" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="project_project_form_view_inherited" model="ir.ui.view">
|
||||
<field name="name">project.project.view.inherited</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="priority">30</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_analytic_account_entries']" position="after">
|
||||
<button name="action_open_project_purchase_orders" type="object" class="oe_stat_button" icon="fa-credit-card" attrs="{'invisible': [('purchase_orders_count', '=', 0)]}" groups="purchase.group_purchase_user">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="purchase_orders_count" nolabel="1"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Purchase Orders
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -35,14 +35,6 @@
|
||||
<button name="action_create_invoice" string="Create Invoice" type="object" class="btn-primary" groups="sales_team.group_sale_salesman_all_leads" attrs="{'invisible': [('has_any_so_to_invoice', '=', False)]}" data-hotkey="w"/>
|
||||
<button name="action_create_invoice" string="Create Invoice" type="object" class="btn-secondary" groups="sales_team.group_sale_salesman_all_leads" attrs="{'invisible': ['|', ('has_any_so_with_nothing_to_invoice', '=', False), ('has_any_so_to_invoice', '=', True)]}" data-hotkey="w"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='project_update_all_action']" position="after">
|
||||
<button class="oe_stat_button"
|
||||
type="object" name="action_view_sos" icon="fa-dollar"
|
||||
attrs="{'invisible': ['|', ('allow_billable', '=', False), ('sale_order_count', '=', 0)]}"
|
||||
groups="sales_team.group_sale_salesman_all_leads">
|
||||
<field name="sale_order_count" widget="statinfo" string="Sales Orders"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_id']" position="attributes">
|
||||
<attribute name="options">{'always_reload': True}</attribute>
|
||||
<attribute name="context">{'res_partner_search_mode': 'customer'}</attribute>
|
||||
@@ -60,32 +52,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_view_analytic_account_entries']" position="after">
|
||||
<button name="action_open_project_invoices" type="object" class="oe_stat_button" icon="fa-pencil-square-o"
|
||||
attrs="{'invisible': ['|', ('invoice_count', '=', 0), ('analytic_account_id', '=', False)]}"
|
||||
groups="account.group_account_readonly">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="invoice_count" nolabel="1"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Invoices
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_view_analytic_account_entries']" position="after">
|
||||
<button name="action_open_project_vendor_bills" type="object" class="oe_stat_button" icon="fa-pencil-square-o" attrs="{'invisible': [('vendor_bill_count', '=', 0)]}" groups="account.group_account_readonly">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="vendor_bill_count" nolabel="1"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
Vendor Bills
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_sale_project_inherit_form" model="ir.ui.view">
|
||||
|
||||
@@ -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,
|
||||
}, {
|
||||
|
||||
@@ -6,23 +6,6 @@
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='%(project.action_project_task_burndown_chart_report)d']" position="after">
|
||||
<button string="Costs / Revenues" class="oe_stat_button d-none" type="object" name="action_view_timesheet" icon="fa-puzzle-piece" attrs="{'invisible': [('allow_billable', '=', False)]}" groups="project.group_project_manager"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_show_timesheets_by_employee_invoice_type']" position="after">
|
||||
<button name="action_billable_time_button" type="object" class="oe_stat_button" icon="fa-clock-o" attrs="{'invisible': ['|', ('allow_timesheets', '=', False), ('analytic_account_id', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_approver">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<div class="oe_inline">
|
||||
<span class="o_stat_value me-1">
|
||||
<field name="billable_percentage" widget="statinfo" nolabel="1"/>%
|
||||
</span>
|
||||
</div>
|
||||
<span class="o_stat_text">
|
||||
Billable Time
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='settings']" position="after">
|
||||
<page name="billing_employee_rate" string="Invoicing" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]}">
|
||||
<group>
|
||||
@@ -31,8 +14,8 @@
|
||||
<field name="pricing_type" invisible="1" widget="radio"/>
|
||||
<field name="timesheet_product_id" string="Default Service" invisible="1" context="{'default_detailed_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
|
||||
<field name="sale_order_id" invisible="1" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
|
||||
<field name="sale_line_id" groups="!sales_team.group_sale_salesman" string="Default Sales Order Item" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
|
||||
<field name="sale_line_id" groups="sales_team.group_sale_salesman" string="Default Sales Order Item" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
|
||||
<field name="sale_line_id" groups="!sales_team.group_sale_salesman" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
|
||||
<field name="sale_line_id" groups="sales_team.group_sale_salesman" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="sale_line_employee_ids">
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user