This commit introduces some adjustments to the dark mode color scheme and the use of bootstrap classes in Community. - Mini calendar contrast - The colors were not using variables, making them non dynamic and breaking the contrast in dark mode. - Avoid !important rules spreadsheet - Prior to this commit, spreadsheet top bar was using a `bg-white` class, making it pure black in dark mode. Since spreadsheet is designed with light colors and we don't provide a dark mode for it, we remove that class and set a `background-color` property using CSS in Enterprise. - Input color - This commit fixes the focus behavior on the searchbar in the control panel, the `command_palette_search`, and the `start a conversation` in discuss. - Copy clipboard field border color - Make use of the `text-primary` color for the copy to clipboard field We use the o-theme-color function to avoid an undefined since primary doesn't exist in the o-theme-text-color map in white mode. - Web_editor toolbar variables - The toolbar was using the `o-brand-primary` variable which was set to a darker shade. This caused issue when activating an option due to how vibrant the color is. - Improve the controls on border-color - Introducing a custom property on the border-color to allow further control on individual components (such as the popover). - Improve setting tabs colors use - Prior to this commit, the colors of the settings tabs menu were kinda inverted. The menu was light in dark mode and dark in light mode. We fix this by changing the values associated to the CSS variables in use. - Make model field selector dark mode proof - Fixing the design of the model field selector popover in both light and dark mode. Since the popover was using custom style with arbitrary values, it was not designed for the dark mode and had a lack of consistency. To improve the design, we use variables rather than custom style, and make sure the desired render is as close as before. - Sign colors use - This commit aims to improve the sign module in both light and dark mode. There were some readability issue with some `btn-light` having poor contrasts in both light and dark mode, and the use of some classes was a bit unexpected (e.g `card-header` to set a grey background with some padding). - Improve buttons design inside listview - Prior to this commit, this button was using custom CSS to make it look like a primary button, while it was using classes related to secondary buttons. We remove the custom CSS used to style it correctly and keep our button design consistent. - Messaging menu layout in mail - This commit aims to improve the design of the notifications displayed in the messaging menu. Prior to this commit, the notifications dropdown was using custom CSS variables overriding the regular behavior of our dropdowns. In fact, the layout was generating some friction: 1) Marking a notification as read would turn its background into a darker color 2) Effects like `:hover` were all based on the custom CSS variables resulting in an inconsistent layout. - Multi company selector adaptations - In darkmode the multi company selection was using the btn-light which creates a weird effect and overrides the dropdown default hover behavior This commit uses the btn-link to display an hover effect on the company switch and on the checkbox while blending with the background and the default dropdown hover effect. - Adapts default badge design - Improve the design of the default badges in dark mode. If you open the light mode, these badges are dark grey with a white text. If you switch to dark mode, they are dark grey but with a dark text, which makes them look either muted or off. We make use of SCSS variables to handle the color of the component, providing a good styling in both modes. - Fix kanban cards borders inside dropdown - Fixes the issue with the divider inside the kanban dropdown menu not showing in dark mode. To ensure it is visible, we assign it the `$dropdown-divider-bg`, which is the color it should use, as the horizontal divider above uses. - Fix tour pointer design for dark mode - This commit aims to insert the tour pointer and its content inside the styling we applied to our tooltip. To do so, we make sure it uses CSS variables, allowing more control and consistency, plus we replicate the overall look of our tooltips. - Fix `text-primary` on action background contrast - This commit improves the readability of our `text-primary` classes when it's used on a `$o-component-active-bg` background. Prior to this commit, the `text-primary` was not meeting the contrast standard, mainly when you were using the `CMD+K` shortcut on the app switcher. To prevent that, we changed the background to a `$o-component-active-bg` background with an opacity ensuring our text provides a good contrast. - Fix input states - Prior to this commit, the `--o-input-border-color` CSS variable was using the `$o-form-lightsecondary` variable to define the standard color of the `border-bottom` property of our inputs. This was conflicting since `$o-form-light-secondary` is also used to define the `background-color` of our table on focus. With this commit, we separate these two element with different variables to make sure they don't affect each others. - Fix kanban ghost background - Before this commit, if you created a project without any stage or element in it, the ghost cards that act like placeholders would be pure `#000` in dark mode, due to the `bg-white` class. This commit replaces that class with a `bg-light`, providing a better visual result in both light and dark mode. - Fix new message design - Improve the design of the new message element while inside Discuss, using our danger color, ensuring a good visual result in both modes. task-3201038 closes odoo/odoo#139966 Related: odoo/enterprise#49666 Signed-off-by: Pierre Paridans (app) <app@odoo.com> Co-authored-by: chgo-odoo <chgo@odoo.com> Co-authored-by: stefanorigano <sri@odoo.com>
193 lines
8.2 KiB
Python
193 lines
8.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
from werkzeug.urls import url_encode
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.osv import expression
|
|
from odoo.tools import formatLang
|
|
|
|
STATUS_COLOR = {
|
|
'on_track': 20, # green / success
|
|
'at_risk': 22, # orange
|
|
'off_track': 23, # red / danger
|
|
'on_hold': 21, # light blue
|
|
'done': 24, # purple
|
|
False: 0, # default grey -- for studio
|
|
# Only used in project.task
|
|
'to_define': 0,
|
|
}
|
|
|
|
class ProjectUpdate(models.Model):
|
|
_name = 'project.update'
|
|
_description = 'Project Update'
|
|
_order = 'id desc'
|
|
_inherit = ['mail.thread.cc', 'mail.activity.mixin']
|
|
|
|
def default_get(self, fields):
|
|
result = super().default_get(fields)
|
|
if 'project_id' in fields and not result.get('project_id'):
|
|
result['project_id'] = self.env.context.get('active_id')
|
|
if result.get('project_id'):
|
|
project = self.env['project.project'].browse(result['project_id'])
|
|
if 'progress' in fields and not result.get('progress'):
|
|
result['progress'] = project.last_update_id.progress
|
|
if 'description' in fields and not result.get('description'):
|
|
result['description'] = self._build_description(project)
|
|
if 'status' in fields and not result.get('status'):
|
|
# `to_define` is not an option for self.status, here we actually want to default to `on_track`
|
|
# the goal of `to_define` is for a project to start without an actual status.
|
|
result['status'] = project.last_update_status if project.last_update_status != 'to_define' else 'on_track'
|
|
return result
|
|
|
|
name = fields.Char("Title", required=True, tracking=True)
|
|
status = fields.Selection(selection=[
|
|
('on_track', 'On Track'),
|
|
('at_risk', 'At Risk'),
|
|
('off_track', 'Off Track'),
|
|
('on_hold', 'On Hold'),
|
|
('done', 'Done'),
|
|
], required=True, tracking=True)
|
|
color = fields.Integer(compute='_compute_color')
|
|
progress = fields.Integer(tracking=True)
|
|
progress_percentage = fields.Float(compute='_compute_progress_percentage')
|
|
user_id = fields.Many2one('res.users', string='Author', required=True, default=lambda self: self.env.user)
|
|
description = fields.Html()
|
|
date = fields.Date(default=fields.Date.context_today, tracking=True)
|
|
project_id = fields.Many2one('project.project', required=True)
|
|
name_cropped = fields.Char(compute="_compute_name_cropped")
|
|
task_count = fields.Integer("Task Count", readonly=True)
|
|
closed_task_count = fields.Integer("Closed Task Count", readonly=True)
|
|
closed_task_percentage = fields.Integer("Closed Task Percentage", compute="_compute_closed_task_percentage")
|
|
|
|
@api.depends('status')
|
|
def _compute_color(self):
|
|
for update in self:
|
|
update.color = STATUS_COLOR[update.status]
|
|
|
|
@api.depends('progress')
|
|
def _compute_progress_percentage(self):
|
|
for update in self:
|
|
update.progress_percentage = update.progress / 100
|
|
|
|
@api.depends('name')
|
|
def _compute_name_cropped(self):
|
|
for update in self:
|
|
update.name_cropped = (update.name[:57] + '...') if len(update.name) > 60 else update.name
|
|
|
|
def _compute_closed_task_percentage(self):
|
|
for update in self:
|
|
update.closed_task_percentage = update.task_count and round(update.closed_task_count * 100 / update.task_count)
|
|
|
|
# ---------------------------------
|
|
# ORM Override
|
|
# ---------------------------------
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
updates = super().create(vals_list)
|
|
for update in updates:
|
|
project = update.project_id
|
|
project.sudo().last_update_id = update
|
|
update.write({
|
|
"task_count": project.task_count,
|
|
"closed_task_count": project.closed_task_count,
|
|
})
|
|
return updates
|
|
|
|
def unlink(self):
|
|
projects = self.project_id
|
|
res = super().unlink()
|
|
for project in projects:
|
|
project.last_update_id = self.search([('project_id', "=", project.id)], order="date desc", limit=1)
|
|
return res
|
|
|
|
# ---------------------------------
|
|
# Build default description
|
|
# ---------------------------------
|
|
@api.model
|
|
def _build_description(self, project):
|
|
return self.env['ir.qweb']._render('project.project_update_default_description', self._get_template_values(project))
|
|
|
|
@api.model
|
|
def _get_template_values(self, project):
|
|
milestones = self._get_milestone_values(project)
|
|
return {
|
|
'user': self.env.user,
|
|
'project': project,
|
|
'show_activities': milestones['show_section'],
|
|
'milestones': milestones,
|
|
'format_lang': lambda value, digits: formatLang(self.env, value, digits=digits),
|
|
}
|
|
|
|
@api.model
|
|
def _get_milestone_values(self, project):
|
|
Milestone = self.env['project.milestone']
|
|
if not project.allow_milestones:
|
|
return {
|
|
'show_section': False,
|
|
'list': [],
|
|
'updated': [],
|
|
'last_update_date': None,
|
|
'created': []
|
|
}
|
|
list_milestones = Milestone.search(
|
|
[('project_id', '=', project.id),
|
|
'|', ('deadline', '<', fields.Date.context_today(self) + relativedelta(years=1)), ('deadline', '=', False)])._get_data_list()
|
|
updated_milestones = self._get_last_updated_milestone(project)
|
|
domain = [('project_id', '=', project.id)]
|
|
if project.last_update_id.create_date:
|
|
domain = expression.AND([domain, [('create_date', '>', project.last_update_id.create_date)]])
|
|
created_milestones = Milestone.search(domain)._get_data_list()
|
|
return {
|
|
'show_section': (list_milestones or updated_milestones or created_milestones) and True or False,
|
|
'list': list_milestones,
|
|
'updated': updated_milestones,
|
|
'last_update_date': project.last_update_id.create_date or None,
|
|
'created': created_milestones,
|
|
}
|
|
|
|
@api.model
|
|
def _get_last_updated_milestone(self, project):
|
|
query = """
|
|
SELECT DISTINCT pm.id as milestone_id,
|
|
pm.deadline as deadline,
|
|
FIRST_VALUE(old_value_datetime::date) OVER w_partition as old_value,
|
|
pm.deadline as new_value
|
|
FROM mail_message mm
|
|
INNER JOIN mail_tracking_value mtv
|
|
ON mm.id = mtv.mail_message_id
|
|
INNER JOIN ir_model_fields imf
|
|
ON mtv.field_id = imf.id
|
|
AND imf.model = 'project.milestone'
|
|
AND imf.name = 'deadline'
|
|
INNER JOIN project_milestone pm
|
|
ON mm.res_id = pm.id
|
|
WHERE mm.model = 'project.milestone'
|
|
AND mm.message_type = 'notification'
|
|
AND pm.project_id = %(project_id)s
|
|
"""
|
|
if project.last_update_id.create_date:
|
|
query = query + "AND mm.date > %(last_update_date)s"
|
|
query = query + """
|
|
WINDOW w_partition AS (
|
|
PARTITION BY pm.id
|
|
ORDER BY mm.date ASC
|
|
)
|
|
ORDER BY pm.deadline ASC
|
|
LIMIT 1;
|
|
"""
|
|
query_params = {'project_id': project.id}
|
|
if project.last_update_id.create_date:
|
|
query_params['last_update_date'] = project.last_update_id.create_date
|
|
self.env.cr.execute(query, query_params)
|
|
results = self.env.cr.dictfetchall()
|
|
mapped_result = {res['milestone_id']: {'new_value': res['new_value'], 'old_value': res['old_value']} for res in results}
|
|
milestones = self.env['project.milestone'].search([('id', 'in', list(mapped_result.keys()))])
|
|
return [{
|
|
**milestone._get_data(),
|
|
'new_value': mapped_result[milestone.id]['new_value'],
|
|
'old_value': mapped_result[milestone.id]['old_value'],
|
|
} for milestone in milestones]
|