[IMP] project, hr_timesheet, *: improve generic ux
Purpose of this commit to improve generic UX for the project app. So, In this commit done following changes: - add 'subtask_effective_hours' and 'total_hours_spent' fields in the page of subtask in task form view. - change invisible attrs to column_invisible attrs for block_by page in task form view. - add 'task in progress' subtype for project.task and move 'stage changed' subtype above 'task blocked'. - remove the 'is_close' field and all its references from project.task.type. - unarchive all of its tasks when the user unarchives the task stage. - sort projects on 'is_favorite'. - internal users following the project automatically follow its updates and their notification preferences propagated accordingly. - duplicating a task will duplicate all of its subtasks. - move parent task field in extra info page of task form view. - change the label of 'edit' to 'view' in the burger menu of project kanban view. - user can delete task without sale_order validation error. - add constraints for 'seller_ids' field. - add remaining_hours_so field in task tree view. - add unusual_days in task calendar view. - add 'no content helper' in action of burndown chart. task-2536044 closes odoo/odoo#74018 Related: odoo/enterprise#19880 Related: odoo/upgrade#2801 Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com> Co-authored-by: Kartik Chavda <kcv@odoo.com>
This commit is contained in:
committed by
Laurent Stukkens (LTU)
co-authored by
Kartik Chavda
parent
6b9183a322
commit
f4c054e473
@@ -1,18 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import pytz
|
||||
from pytz import UTC
|
||||
from datetime import datetime, time
|
||||
from dateutil.rrule import rrule, DAILY
|
||||
from random import choice
|
||||
from string import digits
|
||||
from werkzeug.urls import url_encode
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv.query import Query
|
||||
from odoo.exceptions import ValidationError, AccessError, UserError
|
||||
from odoo.exceptions import ValidationError, AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools.misc import format_date
|
||||
|
||||
@@ -438,14 +436,10 @@ class HrEmployeePrivate(models.Model):
|
||||
if not self:
|
||||
return {}
|
||||
self.ensure_one()
|
||||
calendar = self.resource_calendar_id
|
||||
if not calendar:
|
||||
return {}
|
||||
dfrom = datetime.combine(fields.Date.from_string(date_from), time.min).replace(tzinfo=pytz.UTC)
|
||||
dto = datetime.combine(fields.Date.from_string(date_to), time.max).replace(tzinfo=pytz.UTC)
|
||||
|
||||
works = {d[0].date() for d in calendar._work_intervals_batch(dfrom, dto)[False]}
|
||||
return {fields.Date.to_string(day.date()): (day.date() not in works) for day in rrule(DAILY, dfrom, until=dto)}
|
||||
return self.resource_calendar_id._get_unusual_days(
|
||||
datetime.combine(fields.Date.from_string(date_from), time.min).replace(tzinfo=UTC),
|
||||
datetime.combine(fields.Date.from_string(date_to), time.max).replace(tzinfo=UTC)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Messaging
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
<field name="planned_hours" widget="timesheet_uom_no_toggle" sum="Initially Planned Hours" optional="hide"/>
|
||||
<field name="effective_hours" widget="timesheet_uom" sum="Effective Hours" optional="hide"/>
|
||||
<field name="remaining_hours" widget="timesheet_uom" sum="Remaining Hours" optional="hide" decoration-danger="progress >= 100" decoration-warning="progress >= 80 and progress < 100"/>
|
||||
<field name="subtask_effective_hours" string="Sub-tasks Hours Spent" widget="timesheet_uom" sum="Sub-tasks Hours Spent" optional="hide"/>
|
||||
<field name="total_hours_spent" string="Total Hours" widget="timesheet_uom" sum="Total Hours" optional="hide"/>
|
||||
<field name="progress" widget="progressbar" optional="hide"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook/page[@name='description_page']" position="after">
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
'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/right_panel/*',
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
<field name="hidden" eval="True"/>
|
||||
<field name="description">Task Created</field>
|
||||
</record>
|
||||
<record id="mt_task_stage" model="mail.message.subtype">
|
||||
<field name="name">Stage Changed</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Stage changed</field>
|
||||
</record>
|
||||
<record id="mt_task_blocked" model="mail.message.subtype">
|
||||
<field name="name">Task Blocked</field>
|
||||
<field name="res_model">project.task</field>
|
||||
@@ -20,11 +26,10 @@
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Task ready for Next Stage</field>
|
||||
</record>
|
||||
<record id="mt_task_stage" model="mail.message.subtype">
|
||||
<field name="name">Stage Changed</field>
|
||||
<record id="mt_task_progress" model="mail.message.subtype">
|
||||
<field name="name">Task in Progress</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Stage changed</field>
|
||||
</record>
|
||||
<record id="mt_task_rating" model="mail.message.subtype">
|
||||
<field name="name">Task Rating</field>
|
||||
@@ -37,6 +42,14 @@
|
||||
<field name="default" eval="False"/>
|
||||
<field name="hidden" eval="True"/>
|
||||
</record>
|
||||
<!-- Update-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_update_create" model="mail.message.subtype">
|
||||
<field name="name">Update Created</field>
|
||||
<field name="res_model">project.update</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Update Created</field>
|
||||
<field name="hidden" eval="True"/>
|
||||
</record>
|
||||
<!-- Project-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_project_stage_change" model="mail.message.subtype">
|
||||
<field name="name">Project Stage Changed</field>
|
||||
@@ -94,4 +107,13 @@
|
||||
<field name="relation_field">project_id</field>
|
||||
<field name="hidden" eval="True"/>
|
||||
</record>
|
||||
<record id="mt_project_update_create" model="mail.message.subtype">
|
||||
<field name="name">Update Created</field>
|
||||
<field name="sequence">16</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="parent_id" ref="mt_update_create"/>
|
||||
<field name="relation_field">project_id</field>
|
||||
<field name="hidden" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -64,14 +64,12 @@
|
||||
<field name="sequence">20</field>
|
||||
<field name="name">Done</field>
|
||||
<field name="fold" eval="True"/>
|
||||
<field name="is_closed" eval="True"/>
|
||||
</record>
|
||||
<record id="project_stage_3" model="project.task.type">
|
||||
<field name="sequence">30</field>
|
||||
<field name="name">Cancelled</field>
|
||||
<field name="legend_done">Ready to reopen</field>
|
||||
<field name="fold" eval="True"/>
|
||||
<field name="is_closed" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="project_project_1" model="project.project">
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import ast
|
||||
import json
|
||||
from pytz import UTC
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta, datetime
|
||||
from datetime import timedelta, datetime, time
|
||||
from random import randint
|
||||
|
||||
from odoo import api, Command, fields, models, tools, SUPERUSER_ID, _
|
||||
@@ -99,7 +100,6 @@ class ProjectTaskType(models.Model):
|
||||
help="Automatically modify the kanban state when the customer replies to the feedback for this stage.\n"
|
||||
" * Good feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n"
|
||||
" * Neutral or bad feedback will set the kanban state to 'blocked' (red bullet).\n")
|
||||
is_closed = fields.Boolean('Closing Stage', help="Tasks in this stage are considered as closed.")
|
||||
disabled_rating_warning = fields.Text(compute='_compute_disabled_rating_warning')
|
||||
|
||||
user_id = fields.Many2one('res.users', 'Stage Owner', index=True)
|
||||
@@ -548,6 +548,7 @@ class Project(models.Model):
|
||||
partners = set(task.message_partner_ids.ids) & set(partner_ids)
|
||||
if partners:
|
||||
task.message_subscribe(partner_ids=list(partners), subtype_ids=task_subtypes)
|
||||
self.update_ids.message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
|
||||
return res
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
@@ -656,10 +657,6 @@ class Project(models.Model):
|
||||
'context': {'search_default_group_date': 1, 'default_account_id': self.analytic_account_id.id}
|
||||
}
|
||||
|
||||
def action_view_kanban_project(self):
|
||||
# [XBO] TODO: remove me in master
|
||||
return
|
||||
|
||||
# ---------------------------------------------
|
||||
# PROJECT UPDATES
|
||||
# ---------------------------------------------
|
||||
@@ -857,7 +854,7 @@ class Task(models.Model):
|
||||
project_id = self.env.context.get('default_project_id')
|
||||
if not project_id:
|
||||
return False
|
||||
return self.stage_find(project_id, [('fold', '=', False), ('is_closed', '=', False)])
|
||||
return self.stage_find(project_id, [('fold', '=', False)])
|
||||
|
||||
@api.model
|
||||
def _default_company_id(self):
|
||||
@@ -962,7 +959,7 @@ class Task(models.Model):
|
||||
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
|
||||
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
|
||||
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
|
||||
is_closed = fields.Boolean(related="stage_id.is_closed", string="Closing Stage", readonly=True, related_sudo=False)
|
||||
is_closed = fields.Boolean(related="stage_id.fold", string="Closing Stage", related_sudo=False, help="Folded in Kanban stages are closing stages.")
|
||||
parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
|
||||
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
|
||||
child_text = fields.Char(compute="_compute_child_text")
|
||||
@@ -1389,8 +1386,7 @@ class Task(models.Model):
|
||||
for task in self:
|
||||
if task.project_id:
|
||||
if task.project_id not in task.stage_id.project_ids:
|
||||
task.stage_id = task.stage_find(task.project_id.id, [
|
||||
('fold', '=', False), ('is_closed', '=', False)])
|
||||
task.stage_id = task.stage_find(task.project_id.id, [('fold', '=', False)])
|
||||
else:
|
||||
task.stage_id = False
|
||||
|
||||
@@ -1431,6 +1427,8 @@ class Task(models.Model):
|
||||
default['name'] = _("%s (copy)", self.name)
|
||||
if self.recurrence_id:
|
||||
default['recurrence_id'] = self.recurrence_id.copy().id
|
||||
if self.allow_subtasks:
|
||||
default['child_ids'] = [child.copy().id for child in self.child_ids]
|
||||
return super(Task, self).copy(default)
|
||||
|
||||
@api.model
|
||||
@@ -1777,7 +1775,7 @@ class Task(models.Model):
|
||||
|
||||
def update_date_end(self, stage_id):
|
||||
project_task_type = self.env['project.task.type'].browse(stage_id)
|
||||
if project_task_type.fold or project_task_type.is_closed:
|
||||
if project_task_type.fold:
|
||||
return {'date_end': fields.Datetime.now()}
|
||||
return {'date_end': False}
|
||||
|
||||
@@ -1910,10 +1908,13 @@ class Task(models.Model):
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'kanban_state_label' in init_values and self.kanban_state == 'blocked':
|
||||
return self.env.ref('project.mt_task_blocked')
|
||||
elif 'kanban_state_label' in init_values and self.kanban_state == 'done':
|
||||
return self.env.ref('project.mt_task_ready')
|
||||
mail_message_subtype_per_kanban_state = {
|
||||
'blocked': 'project.mt_task_blocked',
|
||||
'done': 'project.mt_task_ready',
|
||||
'normal': 'project.mt_task_progress',
|
||||
}
|
||||
if 'kanban_state_label' in init_values and self.kanban_state in mail_message_subtype_per_kanban_state:
|
||||
return self.env.ref(mail_message_subtype_per_kanban_state[self.kanban_state])
|
||||
elif 'stage_id' in init_values:
|
||||
return self.env.ref('project.mt_task_stage')
|
||||
return super(Task, self)._track_subtype(init_values)
|
||||
@@ -2171,6 +2172,14 @@ class Task(models.Model):
|
||||
self.ensure_one()
|
||||
return self.analytic_account_id or self.project_analytic_account_id
|
||||
|
||||
@api.model
|
||||
def get_unusual_days(self, date_from, date_to=None):
|
||||
calendar = self.env.company.resource_calendar_id
|
||||
return calendar._get_unusual_days(
|
||||
datetime.combine(fields.Date.from_string(date_from), time.min).replace(tzinfo=UTC),
|
||||
datetime.combine(fields.Date.from_string(date_to), time.max).replace(tzinfo=UTC)
|
||||
)
|
||||
|
||||
class ProjectTags(models.Model):
|
||||
""" Tags of project's tasks """
|
||||
_name = "project.tags"
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<separator/>
|
||||
<filter string="Unassigned Tasks" name="unassigned" domain="[('user_ids', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False), ('stage_id.is_closed', '=', False)]"/>
|
||||
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter name="filter_date_deadline" date="date_deadline"/>
|
||||
<filter name="filter_date_assign" date="date_assign"/>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<filter name="filter_date_deadline" date="date_deadline"/>
|
||||
<filter name="filter_date_assign" date="date_assign"/>
|
||||
<filter string="Last Month" invisible="1" name="last_month" domain="[('date','>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False), ('stage_id.is_closed', '=', False)]"/>
|
||||
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Date" name="date" context="{'group_by': 'date'}" />
|
||||
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}" />
|
||||
@@ -56,7 +56,11 @@
|
||||
<field name="search_view_id" ref="project_task_burndown_chart_report_view_search"/>
|
||||
<field name="context">{'search_default_project_id': active_id}</field>
|
||||
<field name="domain">[('display_project_id', '!=', False)]</field>
|
||||
<field name="help">
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No data yet!
|
||||
</p>
|
||||
<p>Analyze how quickly your team is completing your project's tasks and check if everything is going according to plan.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/** @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;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @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);
|
||||
@@ -30,6 +30,8 @@
|
||||
<separator/>
|
||||
<filter string="Late Tasks" name="late" domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Open Tasks" name="open_tasks" domain="[('is_closed', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Rated Tasks" name="rating_task" domain="[('rating_last_value', '!=', 0.0)]" groups="project.group_project_rating"/>
|
||||
<separator/>
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
|
||||
@@ -127,7 +129,6 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="fold"/>
|
||||
<field name="is_closed" groups="base.group_no_one"/>
|
||||
<field name="project_ids" widget="many2many_tags" options="{'color_field': 'color'}" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</group>
|
||||
@@ -691,14 +692,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_card_manage_settings row" groups="project.group_project_manager">
|
||||
<div role="menuitem" aria-haspopup="true" class="col-8">
|
||||
<div class="o_kanban_card_manage_settings row">
|
||||
<div role="menuitem" aria-haspopup="true" class="col-8" groups="project.group_project_manager">
|
||||
<ul class="oe_kanban_colorpicker" data-field="color" role="popup"/>
|
||||
</div>
|
||||
<div role="menuitem" class="col-4">
|
||||
<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>
|
||||
<!-- [XBO] TODO: remove the name attribute in this a tag in master -->
|
||||
<a class="dropdown-item" role="menuitem" type="edit" name="action_view_kanban_project">Edit</a>
|
||||
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_wizard_action)d" type="action" groups="project.group_project_manager">Share</a>
|
||||
<a class="dropdown-item" role="menuitem" type="edit" groups="project.group_project_manager">Edit</a>
|
||||
<a class="dropdown-item" role="menuitem" type="open" groups="!project.group_project_manager">View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -888,11 +889,6 @@
|
||||
options="{'no_open': True}"
|
||||
widget="many2many_avatar_user"
|
||||
domain="[('share', '=', False)]"/>
|
||||
<field name="parent_id"
|
||||
domain="[('parent_id', '=', False)]"
|
||||
attrs="{'invisible' : [('allow_subtasks', '=', False)]}"
|
||||
groups="base.group_no_one"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" invisible="1"/>
|
||||
@@ -911,7 +907,7 @@
|
||||
<field name="description" type="html" options="{'collaborative': true}"/>
|
||||
</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}">
|
||||
<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}" widget="subtasks_one2many">
|
||||
<tree editable="bottom">
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="is_closed" invisible="1"/>
|
||||
@@ -989,6 +985,7 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" context="{'default_partner_id': partner_id}"/>
|
||||
<field name="parent_id" domain="[('parent_id', '=', False)]" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
|
||||
<field name="sequence" groups="base.group_no_one"/>
|
||||
<field name="email_from" invisible="1"/>
|
||||
@@ -1207,8 +1204,10 @@
|
||||
<field name="model">project.task</field>
|
||||
<field eval="2" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<calendar date_start="date_deadline" string="Tasks" mode="month" color="user_ids" event_limit="5"
|
||||
hide_time="true" js_class="project_calendar" event_open_popup="true" quick_add="false">
|
||||
<calendar date_start="date_deadline" string="Tasks" mode="month"
|
||||
color="user_ids" event_limit="5" hide_time="true"
|
||||
event_open_popup="true" quick_add="false" show_unusual_days="True"
|
||||
js_class="project_calendar">
|
||||
<field name="project_id" widget="project_private_task" filters="1"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user"/>
|
||||
<field name="partner_id" attrs="{'invisible': [('partner_id', '=', False)]}"/>
|
||||
|
||||
@@ -636,6 +636,18 @@ class ResourceCalendar(models.Model):
|
||||
)
|
||||
return interval_dt(work_intervals[0]) if work_intervals else None
|
||||
|
||||
def _get_unusual_days(self, start_dt, end_dt):
|
||||
if not self:
|
||||
return {}
|
||||
self.ensure_one()
|
||||
if not start_dt.tzinfo:
|
||||
start_dt = start_dt.replace(tzinfo=utc)
|
||||
if not end_dt.tzinfo:
|
||||
end_dt = end_dt.replace(tzinfo=utc)
|
||||
|
||||
works = {d[0].date() for d in self._work_intervals_batch(start_dt, end_dt)[False]}
|
||||
return {fields.Date.to_string(day.date()): (day.date() not in works) for day in rrule(DAILY, start_dt, until=end_dt)}
|
||||
|
||||
# --------------------------------------------------
|
||||
# External API
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -169,11 +169,6 @@ class ProjectTask(models.Model):
|
||||
product_id=task.sale_line_id.product_id.display_name,
|
||||
))
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_linked_so(self):
|
||||
if any(task.sale_line_id for task in self):
|
||||
raise ValidationError(_('You have to unlink the task from the sale order item in order to delete it.'))
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Actions
|
||||
# ---------------------------------------------------
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
@@ -13,6 +14,11 @@ class ProductTemplate(models.Model):
|
||||
('service_to_purchase', "CHECK((type != 'service' AND service_to_purchase != true) or (type = 'service'))", 'Product that is not a service can not create RFQ.'),
|
||||
]
|
||||
|
||||
@api.constrains('service_to_purchase', 'seller_ids')
|
||||
def validate_service_to_purchase(self):
|
||||
if self.service_to_purchase and not self.seller_ids:
|
||||
raise ValidationError("Please define the vendor from whom you would like to purchase this service automatically.")
|
||||
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
res = super(ProductTemplate, self)._onchange_type()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
from odoo import Command
|
||||
|
||||
class TestCommonSalePurchaseNoChart(TestSaleCommon):
|
||||
|
||||
@@ -24,6 +24,19 @@ class TestCommonSalePurchaseNoChart(TestSaleCommon):
|
||||
'email': 'supplier.serv@supercompany.com',
|
||||
})
|
||||
|
||||
cls.supplierinfo1 = cls.env['product.supplierinfo'].create({
|
||||
'partner_id': cls.partner_vendor_service.id,
|
||||
'price': 100,
|
||||
'delay': 1,
|
||||
})
|
||||
cls.supplierinfo2 = cls.env['product.supplierinfo'].create({
|
||||
'partner_id': cls.partner_vendor_service.id,
|
||||
'price': 10,
|
||||
'delay': 5,
|
||||
})
|
||||
|
||||
# Create product
|
||||
# When service_to_purser is True add the supplier i.e 'saller_ids' on the product to void the Validation error at product creation time
|
||||
cls.service_purchase_1 = cls.env['product.product'].create({
|
||||
'name': "Out-sourced Service 1",
|
||||
'standard_price': 200.0,
|
||||
@@ -38,6 +51,7 @@ class TestCommonSalePurchaseNoChart(TestSaleCommon):
|
||||
'taxes_id': False,
|
||||
'categ_id': cls.product_category_purchase.id,
|
||||
'service_to_purchase': True,
|
||||
'seller_ids': [Command.set(cls.supplierinfo1.ids)],
|
||||
})
|
||||
cls.service_purchase_2 = cls.env['product.product'].create({
|
||||
'name': "Out-sourced Service 2",
|
||||
@@ -53,17 +67,8 @@ class TestCommonSalePurchaseNoChart(TestSaleCommon):
|
||||
'taxes_id': False,
|
||||
'categ_id': cls.product_category_purchase.id,
|
||||
'service_to_purchase': True,
|
||||
'seller_ids': [Command.set(cls.supplierinfo2.ids)],
|
||||
})
|
||||
|
||||
cls.supplierinfo1 = cls.env['product.supplierinfo'].create({
|
||||
'partner_id': cls.partner_vendor_service.id,
|
||||
'price': 100,
|
||||
'product_tmpl_id': cls.service_purchase_1.product_tmpl_id.id,
|
||||
'delay': 1,
|
||||
})
|
||||
cls.supplierinfo2 = cls.env['product.supplierinfo'].create({
|
||||
'partner_id': cls.partner_vendor_service.id,
|
||||
'price': 10,
|
||||
'product_tmpl_id': cls.service_purchase_2.product_tmpl_id.id,
|
||||
'delay': 5,
|
||||
})
|
||||
cls.supplierinfo1.product_tmpl_id = cls.service_purchase_1.product_tmpl_id.id
|
||||
cls.supplierinfo2.product_tmpl_id = cls.service_purchase_2.product_tmpl_id.id
|
||||
|
||||
@@ -92,9 +92,9 @@ class TestSaleService(TestCommonSaleTimesheet):
|
||||
# delete timesheets before deleting the task, so as to trigger the error
|
||||
# about linked sales order lines and not the one about linked timesheets
|
||||
task.timesheet_ids.unlink()
|
||||
# not possible to delete a task linked to a SOL
|
||||
with self.assertRaises(ValidationError):
|
||||
task.unlink()
|
||||
# unlink automatically task from the SOL when deleting the task
|
||||
task.unlink()
|
||||
self.assertFalse(sale_order_line.task_id, "Deleting the task its should automatically unlink the task from SOL.")
|
||||
|
||||
def test_timesheet_uom(self):
|
||||
""" Test timesheet invoicing and uom conversion """
|
||||
|
||||
@@ -134,6 +134,17 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_task_tree2_inherited" model="ir.ui.view">
|
||||
<field name="name">project.task.tree.inherited</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.view_task_tree2_inherited" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='remaining_hours']" position="after">
|
||||
<field name="remaining_hours_so" string="Remaining Hours on SO" widget="timesheet_uom" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form_inherit_sale_timesheet" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit.timesheet</field>
|
||||
<field name="model">project.task</field>
|
||||
|
||||
Reference in New Issue
Block a user