[ADD] todo, note, *: replace Notes with To-do
- Impacted modules:
- todo (new module)
- note (deprecated app)
- project
- test_discuss_full (adding a dependency)
- Main changes:
- The Notes app is deprecated
- A To-do app is created to replace it
- The To-do app uses the model project.task (from module project)
for to-dos. To-dos are equivalent to private tasks (tasks without
project) with no parent task set.
- Private tasks/to-dos can either be displayed in the My Tasks menu
of the Project app or in the To-do app that proposes simplified
views to edit them.
- Details:
- depracation of Notes: Before the module note can be fully removed,
some changes still need to be done in other modules that use its
models (mainly note.note model). This commit deprecate the Notes
app in the meantime by removing it from the main menu/dashboard
and by replacing the creation of notes directly from the systray
by the creation of private tasks (to-dos).
- activity: In the systray, it is currently not possible to
segregate records using the same model. Therefore to-dos will
appear in the task section. This will change in the future when
this possibility will be implemented in mail.
- name generation: When edited from To-do, a name is auto-genrated
for private tasks. This behavior is similar to the one existing in
notes with a few adaptation to Todo UX. When a to-do is untitled
and saved (through the save button or when leaving the form view),
a name is generated based on the first line of the description of
this to-do. If the generated name is longer than 100 characters it
is truncated.
- security:
- project.task model need to be accessible in all crud modes to
all internal users as it is used in the to-do module. This is
a big change compared to the previous behavior where only
project users could edit tasks.
- project.tags model access is way more permisive now. All users
have access to edition/delition of tags as they are use in the
to-do app.
- Record rules: A similar behavior as the current one is
implemented, i.e:
- project users can access all tasks (with edition rights)
verifying one of the following conditions:
- they follow the task
- they follow the project the task belongs to
- they are assigned to the task
- the project the task belongs to is accessible for all
internal users.
- internal users have only read-access to the non-private
tasks shared with them.
- Those changes related to security only occurs when the app
To-do is installed. When it is note, the current behavior of
Project security-wise is kept unchanged.
Note on this last point: As the write access of internal users to
project.task is now managed with record rules, the form of this model
won't be displayed in read-only anymore when To-do is installed. This
will results in access error message for the tasks for which the write
access is restricted. Unfortunatelly, there is no better solution at the
moment as neither restricted read access, nor extendent write access for
internal users are desired. An improvement of the framework to deal with
such situation could be benificial.
task-3085077
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import models
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
'data': [
|
||||
'security/note_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'data/note_data.xml',
|
||||
'data/res_users_data.xml',
|
||||
'views/note_views.xml',
|
||||
@@ -28,9 +27,6 @@
|
||||
'web.assets_backend': [
|
||||
'note/static/src/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'note/static/tests/**/*',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class NoteController(http.Controller):
|
||||
|
||||
@http.route('/note/new', type='json', auth='user')
|
||||
def note_new_from_systray(self, note, activity_type_id=None, date_deadline=None):
|
||||
""" Route to create note and their activity directly from the systray """
|
||||
note = request.env['note.note'].create({'memo': note})
|
||||
if date_deadline:
|
||||
note.activity_schedule(
|
||||
activity_type_id=activity_type_id or request.env['mail.activity.type'].search([('category', '=', 'reminder')], limit=1).id,
|
||||
note=note.memo,
|
||||
date_deadline=date_deadline
|
||||
)
|
||||
return note.id
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_activity
|
||||
from . import mail_activity_type
|
||||
from . import note
|
||||
from . import res_users
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class MailActivity(models.Model):
|
||||
_inherit = "mail.activity"
|
||||
|
||||
note_id = fields.Many2one('note.note', string="Related Note", ondelete='cascade')
|
||||
@@ -45,30 +45,3 @@ GROUP BY id"""
|
||||
stage.sudo().copy(default={'user_id': user.id})
|
||||
else:
|
||||
_logger.debug("Created note columns for %s", self)
|
||||
|
||||
@api.model
|
||||
def systray_get_activities(self):
|
||||
""" If user have not scheduled any note, it will not appear in activity menu.
|
||||
Making note activity always visible with number of notes on label. If there is no notes,
|
||||
activity menu not visible for note.
|
||||
"""
|
||||
activities = super(Users, self).systray_get_activities()
|
||||
notes_count = self.env['note.note'].search_count([('user_id', '=', self.env.uid)])
|
||||
if notes_count:
|
||||
note_index = next((index for (index, a) in enumerate(activities) if a["model"] == "note.note"), None)
|
||||
note_label = _('Notes')
|
||||
if note_index is not None:
|
||||
activities[note_index]['name'] = note_label
|
||||
else:
|
||||
activities.append({
|
||||
'id': self.env['ir.model']._get('note.note').id,
|
||||
'type': 'activity',
|
||||
'name': note_label,
|
||||
'model': 'note.note',
|
||||
'icon': modules.module.get_module_icon(self.env['note.note']._original_module),
|
||||
'total_count': 0,
|
||||
'today_count': 0,
|
||||
'overdue_count': 0,
|
||||
'planned_count': 0
|
||||
})
|
||||
return activities
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
// ensure mail override is applied first.
|
||||
import '@mail/../tests/helpers/mock_server/models/res_users';
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
patch(MockServer.prototype, 'note/models/res_users', {
|
||||
/**
|
||||
* Simulates `systray_get_activities` on `res.users`.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mockResUsersSystrayGetActivities() {
|
||||
const activities = this._super(...arguments);
|
||||
const noteCount = this.pyEnv['note.note'].searchCount([['user_id', '=', this.currentUserId]]);
|
||||
if (noteCount) {
|
||||
const noteIndex = activities.findIndex(act => act['model'] === 'note.note');
|
||||
if (noteIndex) {
|
||||
activities[noteIndex]['name'] = 'Notes';
|
||||
} else {
|
||||
activities.push({
|
||||
id: 'note.note', // for simplicity
|
||||
type: 'activity',
|
||||
name: 'Notes',
|
||||
model: 'note.note',
|
||||
planned_count: 0,
|
||||
today_count: 0,
|
||||
overdue_count: 0,
|
||||
total_count: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
},
|
||||
});
|
||||
@@ -171,6 +171,9 @@
|
||||
</div>
|
||||
<field name="stage_id" domain="[('user_id','=',uid)]" widget="statusbar" options="{'clickable': '1'}"/>
|
||||
</header>
|
||||
<div class="alert alert-warning oe_edit_only mb-0" role="status">
|
||||
<p>The Notes app is deprecated and has been replaced by Todo. All your existing notes have been converted to to-dos that you will find in this new app. The Notes app will be removed in the future. It is therefore adviced to not use it anymore as all the notes that would be created/modified in it will be lost during future upgrade.</p>
|
||||
</div>
|
||||
<sheet>
|
||||
<field name="memo" type="html" class="oe_memo" default_focus="1" options="{'resizable': false, 'collaborative': true}"/>
|
||||
</sheet>
|
||||
@@ -225,27 +228,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_note_notes"
|
||||
name="Notes"
|
||||
sequence="15"
|
||||
action="note.action_note_note"
|
||||
web_icon="note,static/description/icon.svg">
|
||||
<menuitem
|
||||
id="menu_note_configuration"
|
||||
name="Configuration"
|
||||
sequence="100"
|
||||
groups="base.group_no_one">
|
||||
<menuitem
|
||||
id="menu_notes_stage"
|
||||
name="Stages"
|
||||
action="note.action_note_stage"
|
||||
sequence="21"/>
|
||||
<menuitem
|
||||
id="notes_tag_menu"
|
||||
action="note_tag_action"
|
||||
sequence="22"/>
|
||||
</menuitem>
|
||||
</menuitem>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -91,7 +91,8 @@ class Task(models.Model):
|
||||
|
||||
@api.model
|
||||
def _default_personal_stage_type_id(self):
|
||||
return self.env['project.task.type'].search([('user_id', '=', self.env.user.id)], limit=1).id
|
||||
default_id = self.env.context.get('default_personal_stage_type_ids')
|
||||
return (default_id or self.env['project.task.type'].search([('user_id', '=', self.env.user.id)], limit=1).ids or [False])[0]
|
||||
|
||||
@api.model
|
||||
def _default_company_id(self):
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
'mail',
|
||||
'mail_bot',
|
||||
'note',
|
||||
'project_todo',
|
||||
'website_livechat',
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
||||
def _todo_uninstall(env):
|
||||
# The record rule project.task_visibility_rule needs to apply to all rights and not just read after uninstallation
|
||||
project_task_visibility_rule_rec = env.ref("project.task_visibility_rule")
|
||||
project_task_visibility_rule_rec.write({
|
||||
'perm_create': True,
|
||||
'perm_unlink': True,
|
||||
'perm_write': True,
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'To-Do',
|
||||
'version': '1.0',
|
||||
'category': 'Productivity/To-Do',
|
||||
'summary': 'Organize your work with memos and to-do lists',
|
||||
'sequence': 260,
|
||||
'depends': [
|
||||
'project',
|
||||
],
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/todo_security.xml',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'views/project_task_views.xml',
|
||||
'views/todo_menus.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'uninstall_hook': '_todo_uninstall',
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'todo/static/src/scss/todo.scss',
|
||||
'todo/static/src/views/**/*',
|
||||
'todo/static/src/web/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'todo/static/tests/**/*',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import note
|
||||
from . import todo
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import Controller, request, route
|
||||
|
||||
class TodoController(Controller):
|
||||
|
||||
@route('/project_todo/new', type='json', auth='user')
|
||||
def todo_new_from_systray(self, todo_description, activity_type_id=None, date_deadline=None):
|
||||
""" Route to create todo and their activity directly from the systray """
|
||||
todo = request.env['project.task'].create({'description': todo_description})
|
||||
if date_deadline:
|
||||
todo.activity_schedule(
|
||||
activity_type_id=activity_type_id or request.env['mail.activity.type'].search([('category', '=', 'reminder')], limit=1).id,
|
||||
note=todo.description,
|
||||
date_deadline=date_deadline
|
||||
)
|
||||
return todo.id
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import mail_activity_type
|
||||
from . import project_task
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, _
|
||||
from odoo.tools import html2plaintext
|
||||
|
||||
class Task(models.Model):
|
||||
_inherit = 'project.task'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') and not vals.get('project_id') and not vals.get('parent_id'):
|
||||
if vals.get('description'):
|
||||
# Generating name from first line of the description
|
||||
text = html2plaintext(vals['description'])
|
||||
name = text.strip().replace('*', '').partition("\n")[0]
|
||||
vals['name'] = (name[:97] + '...') if len(name) > 100 else name
|
||||
else:
|
||||
vals['name'] = _('Untitled to-do')
|
||||
return super().create(vals_list)
|
||||
@@ -0,0 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_project_task_type_user,project.task.type.user,project.model_project_task_type,base.group_user,1,1,1,1
|
||||
access_task_on_partner,project.task on partners,project.model_project_task,base.group_user,1,1,1,1
|
||||
access_project_tags_user,project.project_tags_user,project.model_project_tags,base.group_user,1,1,1,1
|
||||
|
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record model="ir.rule" id="project.task_visibility_rule">
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="task_edition_rule_internal">
|
||||
<field name="name">Project/Task: employees: Full access to own private task only</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[('project_id', '=', False), ('user_ids', 'in', user.id), ('parent_id', '=', False)]</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="task_visibility_rule_project_user">
|
||||
<field name="name">Project/Task: project users: follow required for follower-only projects</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
'&',
|
||||
('project_id', '!=', False),
|
||||
'|',
|
||||
('project_id.privacy_visibility', '!=', 'followers'),
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
'|',
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# to subscribe check access to the record, follower is not enough at creation
|
||||
('user_ids', 'in', user.id)
|
||||
]</field>
|
||||
<field name="groups" eval="[(4,ref('project.group_project_user'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="50" height="50" xmlns="http://www.w3.org/2000/svg"><path d="M10 41h36a3 3 0 0 1-3 3H10v-3Z" fill="var(--o-icons-teal-pure, #00CEB3)"/><path d="M10 41h36a3 3 0 0 1-3 3H10v-3Z" fill="url(#o_icon_note__a)" fill-opacity=".2"/><path d="M9 41h19c0 1.657-1.418 3-3.167 3H9v-3Z" fill="var(--o-icons-teal-teal, #00A079)"/><path d="M9 41h19c0 1.657-1.418 3-3.167 3H9v-3Z" fill="url(#o_icon_note__b)" fill-opacity=".2"/><path d="M40.706 7.282a4.35 4.35 0 0 1 0 6.193L9.906 44H4v-6.192L34.457 7.282a4.447 4.447 0 0 1 6.249 0Z" fill="var(--o-icons-purple-teal, #003458)"/><path d="M40.706 7.282a4.35 4.35 0 0 1 0 6.193L9.906 44H4v-6.192L34.457 7.282a4.447 4.447 0 0 1 6.249 0Z" fill="url(#o_icon_note__c)" fill-opacity=".3"/><defs><linearGradient id="o_icon_note__a" x1="10" y1="41" x2="14.5" y2="54.5" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="o_icon_note__b" x1="9" y1="41" x2="15.772" y2="51.723" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="o_icon_note__c" x1="4" y1="6" x2="48.706" y2="17.177" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,40 @@
|
||||
.o_todo_form_view {
|
||||
.o_form_sheet_bg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.o_form_statusbar {
|
||||
margin: 0;
|
||||
.o_field_widget {
|
||||
align-self: center !important;
|
||||
}
|
||||
}
|
||||
.o_form_sheet {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
.oe_pad {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.oe_pad_content {
|
||||
border: none;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_field_html .note-editable {
|
||||
border: none !important;
|
||||
padding: $o-sheet-vpadding $o-horizontal-padding 10px !important;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_form_readonly {
|
||||
.oe_description {
|
||||
padding: $o-sheet-vpadding $o-horizontal-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_todo_kanban_card_body {
|
||||
padding-left: 9px;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.o_form_view.o_xxl_form_view {
|
||||
|
||||
.o_todo_form_sheet_bg {
|
||||
|
||||
> .o_form_statusbar, > .alert {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+27
-22
@@ -7,63 +7,68 @@ import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ActivityMenu.prototype, "note", {
|
||||
patch(ActivityMenu.prototype, "todo", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.rpc = useService("rpc");
|
||||
this.state = useState({ addingNote: false });
|
||||
this.noteInputRef = useRef("noteInput");
|
||||
this.addingNoteDate = false;
|
||||
this.state = useState({ addingTodo: false });
|
||||
this.todoInputRef = useRef("todoInput");
|
||||
this.addingTodoDate = false;
|
||||
this.notification = useService("notification");
|
||||
useEffect(
|
||||
(addingNote) => {
|
||||
if (addingNote) {
|
||||
this.noteInputRef.el.focus();
|
||||
(addingTodo) => {
|
||||
if (addingTodo) {
|
||||
this.todoInputRef.el.focus();
|
||||
}
|
||||
},
|
||||
() => [this.state.addingNote]
|
||||
() => [this.state.addingTodo]
|
||||
);
|
||||
onExternalClick("noteInput", (ev) => {
|
||||
onExternalClick("todoInput", (ev) => {
|
||||
if (
|
||||
ev.target.closest(".o-mail-ActivityMenu-show") ||
|
||||
ev.target.closest(".o_datetime_picker")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.state.addingNote = false;
|
||||
this.state.addingTodo = false;
|
||||
});
|
||||
},
|
||||
|
||||
sortActivityGroups() {
|
||||
this._super();
|
||||
this.store.activityGroups.sort((g1, g2) => {
|
||||
if (g1.model === "note.note" ? true : false) {
|
||||
if (g1.model === "project.task" ? true : false) {
|
||||
return -1;
|
||||
}
|
||||
if (g2.model === "note.note" ? true : false) {
|
||||
if (g2.model === "project.task" ? true : false) {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onKeydownNoteInput(ev) {
|
||||
onKeydownTodoInput(ev) {
|
||||
if (ev.key === "Enter") {
|
||||
this.saveNote();
|
||||
this.saveTodo();
|
||||
}
|
||||
},
|
||||
|
||||
async saveNote() {
|
||||
async saveTodo() {
|
||||
const { DateTime } = luxon;
|
||||
const urlRegExp = /http(s)?:\/\/(www\.)?[a-zA-Z0-9@:%_+~#=~#?&/=\-;!.]{3,2000}/g;
|
||||
const note = this.noteInputRef.el.value.replace(urlRegExp, '<a href="$&">$&</a>').trim();
|
||||
if (!note) {
|
||||
const todo = this.todoInputRef.el.value.replace(urlRegExp, '<a href="$&">$&</a>').trim();
|
||||
if (!todo) {
|
||||
return;
|
||||
}
|
||||
await this.rpc("/note/new", {
|
||||
note: note,
|
||||
date_deadline: this.addingNoteDate ? this.addingNoteDate : DateTime.local(),
|
||||
await this.rpc("/project_todo/new", {
|
||||
todo_description: todo,
|
||||
date_deadline: this.addingTodoDate ? this.addingTodoDate : DateTime.local(),
|
||||
});
|
||||
this.state.addingNote = false;
|
||||
this.fetchSystrayActivities();
|
||||
document.body.click(); // hack to close dropdown
|
||||
this.notification.add(
|
||||
this.env._t("Your to-do has been successfully added to your tasks and scheduled for completion."),
|
||||
{ type: "success" },
|
||||
);
|
||||
this.state.addingTodo = false;
|
||||
},
|
||||
});
|
||||
|
||||
+9
-9
@@ -2,26 +2,26 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.ActivityMenu" t-inherit-mode="extension">
|
||||
<xpath expr="//*[@name='activityGroupLoop']" position="after">
|
||||
<div t-if="!state.addingNote" class="d-grid" t-on-click="() => this.state.addingNote = true">
|
||||
<button type="button" class="btn text-center" t-on-click="() => this.state.addingNote = true">Add new note</button>
|
||||
<div t-if="!state.addingTodo" class="d-grid" t-on-click="() => this.state.addingTodo = true">
|
||||
<button type="button" class="btn text-center" t-on-click="() => this.state.addingTodo = true">Add a To-do</button>
|
||||
</div>
|
||||
<div t-if="state.addingNote" class="o-mail-ActivityMenu-show d-flex p-2 border-bottom">
|
||||
<img src="/note/static/description/icon.svg" alt="Addnote"/>
|
||||
<div t-if="state.addingTodo" class="o-mail-ActivityMenu-show d-flex p-2 border-bottom">
|
||||
<img src="/todo/static/description/icon.svg" alt="AddTodo"/>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex px-2">
|
||||
<span class="o-mail-ActivityMenu-text flex-grow-1"><strong>Add a note</strong></span>
|
||||
<span class="o-mail-ActivityMenu-text flex-grow-1"><strong>Add a To-do</strong></span>
|
||||
<DateTimeInput
|
||||
type="'date'"
|
||||
value="addingNoteDate"
|
||||
onChange="(value) => (this.addingNoteDate = value || false)"
|
||||
value="addingTodoDate"
|
||||
onChange="(value) => (this.addingTodoDate = value || false)"
|
||||
placeholder="'Today'"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<input class="o-mail-ActivityMenu-input py-0 px-2 border-0" type="text" placeholder="Remember..." t-on-keydown="onKeydownNoteInput" t-ref="noteInput"/>
|
||||
<input class="o-mail-ActivityMenu-input py-0 px-2 border-0" type="text" placeholder="Reminder to..." t-on-keydown="onKeydownTodoInput" t-ref="todoInput"/>
|
||||
<span class="flex-grow-1"/>
|
||||
<span class="ml8 mr4">
|
||||
<button type="button" class="o-mail-ActivityMenu-save fw-bold btn btn-link py-0 px-2" t-on-click="saveNote">SAVE</button>
|
||||
<button type="button" class="o-mail-ActivityMenu-save fw-bold btn btn-link py-0 px-2" t-on-click="saveTodo">SAVE</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
+24
-15
@@ -5,13 +5,13 @@ import { getFixture, editInput, patchDate } from "@web/../tests/helpers/utils";
|
||||
|
||||
let target;
|
||||
|
||||
QUnit.module("note activity menu", {
|
||||
QUnit.module("todo activity menu", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("create note from activity menu without date", async function (assert) {
|
||||
QUnit.test("create todo from activity menu without date", async function (assert) {
|
||||
await start();
|
||||
assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']");
|
||||
assert.containsNone(target, ".o-mail-ActivityMenu-counter");
|
||||
@@ -20,28 +20,34 @@ QUnit.test("create note from activity menu without date", async function (assert
|
||||
target,
|
||||
".o-mail-ActivityMenu:contains(Congratulations, you're done with your activities.)"
|
||||
);
|
||||
assert.containsOnce(target, "button:contains(Add new note)");
|
||||
await click("button:contains(Add new note)");
|
||||
assert.containsNone(target, "button:contains(Add new note)");
|
||||
assert.containsOnce(target, "button:contains(Add a To-do)");
|
||||
await click("button:contains(Add a To-do)");
|
||||
assert.containsNone(target, "button:contains(Add a To-do)");
|
||||
assert.containsOnce(target, ".o-mail-ActivityMenu-show");
|
||||
await editInput(target, "input.o-mail-ActivityMenu-input", "New Note");
|
||||
await editInput(target, "input.o-mail-ActivityMenu-input", "New To-do");
|
||||
await click("button:contains(SAVE)");
|
||||
// Need to reopen systray as it automatically closes when a todo is created
|
||||
assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']");
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
assert.containsOnce(target, ".o-mail-ActivityMenu-counter:contains(1)");
|
||||
assert.containsOnce(target, ".o-mail-ActivityGroup");
|
||||
assert.containsOnce(target, ".o-mail-ActivityGroup:contains(note.note)");
|
||||
assert.containsOnce(target, ".o-mail-ActivityGroup:contains(project.task)");
|
||||
assert.containsOnce(target, "button:contains(1 Today)");
|
||||
assert.containsOnce(target, "button:contains(Add new note)");
|
||||
assert.containsOnce(target, "button:contains(Add a To-do)");
|
||||
assert.containsNone(target, ".o-mail-ActivityMenu-show");
|
||||
assert.containsNone(target, ".o-mail-ActivityMenu-input");
|
||||
await click("button:contains(Add new note)");
|
||||
assert.containsNone(target, "button:contains(Add new note)");
|
||||
await editInput(target, "input.o-mail-ActivityMenu-input", "New Note");
|
||||
await click("button:contains(Add a To-do)");
|
||||
assert.containsNone(target, "button:contains(Add a To-do)");
|
||||
await editInput(target, "input.o-mail-ActivityMenu-input", "New To-do");
|
||||
await click("button:contains(SAVE)");
|
||||
// Need to reopen systray as it automatically closes when a todo is created
|
||||
assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']");
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
assert.containsOnce(target, ".o-mail-ActivityMenu-counter:contains(2)");
|
||||
assert.containsOnce(target, "button:contains(2 Today)");
|
||||
});
|
||||
|
||||
QUnit.test("create note from activity menu with date", async function (assert) {
|
||||
QUnit.test("create to-do from activity menu with date", async function (assert) {
|
||||
patchDate(2023, 1, 1, 6, 0, 0);
|
||||
const { DateTime } = luxon;
|
||||
const today = DateTime.utc();
|
||||
@@ -51,16 +57,19 @@ QUnit.test("create note from activity menu with date", async function (assert) {
|
||||
assert.containsNone(target, ".o-mail-ActivityMenu-counter");
|
||||
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
await click("button:contains(Add new note)");
|
||||
await editInput(target, "input.o-mail-ActivityMenu-input", "New Note");
|
||||
await click("button:contains(Add a To-do)");
|
||||
await editInput(target, "input.o-mail-ActivityMenu-input", "New To-do");
|
||||
await editInput(
|
||||
target,
|
||||
"input.o_datetime_input",
|
||||
futureDay.toString(luxon.DateTime.DATE_SHORT)
|
||||
);
|
||||
await click("button:contains(SAVE)");
|
||||
// Need to reopen systray as it automatically closes when a todo is created
|
||||
assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']");
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
assert.containsOnce(target, ".o-mail-ActivityMenu-counter:contains(1)");
|
||||
assert.containsOnce(target, ".o-mail-ActivityGroup");
|
||||
assert.containsOnce(target, ".o-mail-ActivityGroup:contains(note.note)");
|
||||
assert.containsOnce(target, ".o-mail-ActivityGroup:contains(project.task)");
|
||||
assert.containsOnce(target, "button:contains(1 Future)");
|
||||
});
|
||||
+9
-8
@@ -5,7 +5,7 @@ import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
import { date_to_str } from 'web.time';
|
||||
|
||||
patch(MockServer.prototype, 'note/controller/note', {
|
||||
patch(MockServer.prototype, 'project_todo/controller/project_todo', {
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -14,8 +14,8 @@ patch(MockServer.prototype, 'note/controller/note', {
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/note/new') {
|
||||
return this._mockRouteNoteNew(args);
|
||||
if (route === '/project_todo/new') {
|
||||
return this._mockRouteTodoNew(args);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
@@ -25,17 +25,18 @@ patch(MockServer.prototype, 'note/controller/note', {
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulates the `/note/new` route.
|
||||
* Simulates the `/project_todo/new` route.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_mockRouteNoteNew(values) {
|
||||
const noteId = this.pyEnv['note.note'].create({ memo: values['note'] });
|
||||
_mockRouteTodoNew(values) {
|
||||
const taskId = this.pyEnv['project.task'].create({ description: values['todo_description'] });
|
||||
if (values['date_deadline']) {
|
||||
this.pyEnv['mail.activity'].create({
|
||||
date_deadline: date_to_str(new Date(values['date_deadline'])),
|
||||
note_id: noteId,
|
||||
res_model: 'note.note',
|
||||
note: values['todo_description'],
|
||||
res_model: 'project.task',
|
||||
res_id: taskId,
|
||||
});
|
||||
}
|
||||
},
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
|
||||
addModelNamesToFetch(['note.note']);
|
||||
addModelNamesToFetch(['project.task']);
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_access_rights
|
||||
@@ -0,0 +1,20 @@
|
||||
from odoo.tests.common import users
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.addons.project.tests.test_access_rights import TestAccessRights
|
||||
|
||||
class TestAccessRightsTodo(TestAccessRights):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.private_task = cls.env['project.task'].create({'name': 'OdooBot Private Task'})
|
||||
|
||||
@users('Internal user')
|
||||
def test_internal_cannot_rud_private_task(self):
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).write({'name': 'Test write'})
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).unlink()
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
self.private_task.with_user(self.env.user).read(['name'])
|
||||
@@ -0,0 +1,244 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Todo Kanban view -->
|
||||
<record id="project_task_view_todo_kanban" model="ir.ui.view">
|
||||
<field name="name">project.task.kanban</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="priority">800</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="personal_stage_type_ids"
|
||||
class="o_kanban_small_column"
|
||||
on_create="quick_create"
|
||||
quick_create_view="todo.project_task_view_todo_quick_create_form"
|
||||
sample="1"
|
||||
js_class="project_task_kanban"
|
||||
default_order="state, priority desc, date_deadline asc, sequence, id desc">
|
||||
<field name="color"/>
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="active"/>
|
||||
<field name="description"/>
|
||||
<field name="message_partner_ids"/>
|
||||
<field name="activity_ids" />
|
||||
<field name="activity_state" />
|
||||
<field name="is_closed"/>
|
||||
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/>
|
||||
<templates>
|
||||
<t t-name="kanban-menu">
|
||||
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
|
||||
<ul class="oe_kanban_colorpicker" data-field="color"/>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
<t t-set="todoHasAssignees" t-value="record.user_ids.raw_value.length > 1"/>
|
||||
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click_edit oe_semantic_html_override oe_kanban_card">
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<div class="d-flex justify-content-start">
|
||||
<div>
|
||||
<field name="state" widget="todo_done_checkmark" options="{'hide_label': 1}"/>
|
||||
</div>
|
||||
<div t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
|
||||
<strong class="o_kanban_record_title align-middle">
|
||||
<field name="name" widget="name_with_subtask_count"/>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_body o_todo_kanban_card_body" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left"/>
|
||||
<div class="oe_kanban_bottom_right" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
|
||||
<div t-attf-class="d-flex #{todoHasAssignees ? 'w-100 align-items-center justify-content-end' : 'align-items-end'}">
|
||||
<div class="d-flex align-items-center me-2 mt-2">
|
||||
<div t-if="todoHasAssignees">
|
||||
<field name="user_ids" widget="many2many_avatar_user"/>
|
||||
</div>
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- To-do Tree View -->
|
||||
<record id="project_task_view_todo_tree" model="ir.ui.view">
|
||||
<field name="name">project.task.todo.tree</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="To-dos"
|
||||
default_group_by="personal_stage_type_ids"
|
||||
editable="bottom"
|
||||
multi_edit="1"
|
||||
sample="1">
|
||||
<field name="name"/>
|
||||
<field name="user_ids" optional="show" widget="many2many_avatar_user" options="{'no_quick_create': True}"/>
|
||||
<field name="tag_ids" optional="show" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
|
||||
<field name="personal_stage_type_id" string="Stage" optional="hide"/>
|
||||
<field name="activity_ids" optional="show" widget="list_activity"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Todo Form view -->
|
||||
<record id="project_task_view_todo_form" model="ir.ui.view">
|
||||
<field name="name">project.task.view.todo.form</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="To-do"
|
||||
class="o_todo_form_view"
|
||||
js_class="todo_form">
|
||||
<header>
|
||||
<div class="py-1 px-2 border rounded bg-view">
|
||||
<field name="tag_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'color_field': 'color', 'no_create_edit': True}"
|
||||
class="me-2"
|
||||
placeholder="Tags"/>
|
||||
</div>
|
||||
<div class="py-1 px-2 border rounded bg-view">
|
||||
<field name="user_ids"
|
||||
widget="many2many_avatar_user"
|
||||
options="{'no_quick_create': True}"
|
||||
placeholder="Assignees"/>
|
||||
</div>
|
||||
<field name="personal_stage_type_id"
|
||||
domain="[('user_id','=',uid)]"
|
||||
widget="statusbar"
|
||||
options="{'clickable': '1', 'fold_field': 'fold'}"/>
|
||||
<field name="active" invisible="1"/>
|
||||
</header>
|
||||
<sheet class="o_todo_form_sheet_bg">
|
||||
<widget name="web_ribbon" class="todo_archived" title="Archived" bg_color="text-bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<field name="description" type="html" class="oe_description" default_focus="1" options="{'resizable': false, 'collaborative': true}"/>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- To-do Quick create form view -->
|
||||
<record id="project_task_view_todo_quick_create_form" model="ir.ui.view">
|
||||
<field name="name">project.task.view.todo.quick.create.todo</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<form class="o_form_project_tasks">
|
||||
<group>
|
||||
<field name="name" string="To-do Title" placeholder="e.g. Send Invitations"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Todo Activity -->
|
||||
<record id="project_task_view_todo_activity" model="ir.ui.view">
|
||||
<field name="name">project.task.view.todo.activity</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<activity string="To-dos" js_class="project_activity">
|
||||
<field name="user_ids"/>
|
||||
<templates>
|
||||
<div class="w-100" t-name="activity-box" style="display: inline-grid; grid-template-columns: auto max-content;">
|
||||
<div>
|
||||
<span t-att-title="record.name.value">
|
||||
<field name="name" display="full" class="w-100 text-truncate align-middle"/>
|
||||
</span>
|
||||
</div>
|
||||
<field name="user_ids" widget="many2many_avatar_user"/>
|
||||
</div>
|
||||
</templates>
|
||||
</activity>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Todo Search -->
|
||||
<record id="project_task_view_todo_search" model="ir.ui.view">
|
||||
<field name="name">project.task.view.todo.search</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Todos">
|
||||
<field name="name"/>
|
||||
<field name="tag_ids"/>
|
||||
<field name="user_ids"/>
|
||||
<field name="personal_stage_type_ids" string="Stage"/>
|
||||
<filter name="open_tasks" string="Open" domain="[('is_closed', '=', False)]"/>
|
||||
<filter name="closed_tasks" string="Closed" domain="[('is_closed', '=', True)]"/>
|
||||
<filter string="Closed On" name="closed_on" domain="[('is_closed', '=', True)]" date="date_last_stage_update"/>
|
||||
<separator/>
|
||||
<filter name="active_false" string="Archived" domain="[('active', '=', False)]"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Tags" name="tags" help="By assigned tags" context="{'group_by':'tag_ids'}"/>
|
||||
<filter string="Assignees" name="user_ids" context="{'group_by': 'user_ids'}"/>
|
||||
<filter string="Stage" name="stage" help="By personal stages" context="{'group_by':'personal_stage_type_ids'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Todo main action + view links-->
|
||||
<record id="project_task_action_todo" model="ir.actions.act_window">
|
||||
<field name="name">To-dos</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="domain">[('user_ids', 'in', [uid]), ('project_id', '=', False), ('parent_id', '=', False)]</field>
|
||||
<field name="view_mode">kanban,form,tree,activity</field>
|
||||
<field name="search_view_id" ref="project_task_view_todo_search"/>
|
||||
<field name="context">{'search_default_open_tasks': 1, 'tree_view_ref': 'todo.project_task_view_todo_tree'}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No to-do found. Let's create one!
|
||||
</p><p>
|
||||
Keep your work organized by using memos and to-do lists.
|
||||
Your to-do items are private by default, but you can choose to share them with others by adding them as assignees.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="project_task_action_todo_kanban_view">
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="view_id" ref="project_task_view_todo_kanban"/>
|
||||
<field name="act_window_id" ref="project_task_action_todo"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="project_task_action_todo_form_view">
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="project_task_view_todo_form"/>
|
||||
<field name="act_window_id" ref="project_task_action_todo"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="project_task_action_todo_tree_view">
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="project_task_view_todo_tree"/>
|
||||
<field name="act_window_id" ref="project_task_action_todo"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="project_task_action_todo_activity_view">
|
||||
<field name="view_mode">activity</field>
|
||||
<field name="view_id" ref="project_task_view_todo_activity"/>
|
||||
<field name="act_window_id" ref="project_task_action_todo"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="menu_todo_todos"
|
||||
name="To-do"
|
||||
action="todo.project_task_action_todo"
|
||||
web_icon="todo,static/description/icon.svg">
|
||||
</menuitem>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user