diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py index 6e800247cb5..248eee3d103 100644 --- a/addons/analytic/analytic.py +++ b/addons/analytic/analytic.py @@ -96,6 +96,8 @@ class account_analytic_account(osv.osv): return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context) def name_get(self, cr, uid, ids, context=None): + if isinstance(ids, (int, long)): + ids=[ids] if not ids: return [] res = [] @@ -132,7 +134,7 @@ class account_analytic_account(osv.osv): return accounts def _set_company_currency(self, cr, uid, ids, name, value, arg, context=None): - if type(ids) != type([]): + if isinstance(ids, (int, long)): ids=[ids] for account in self.browse(cr, uid, ids, context=context): if account.company_id: diff --git a/addons/project/__openerp__.py b/addons/project/__openerp__.py index 35b920828b2..68c85ffb836 100644 --- a/addons/project/__openerp__.py +++ b/addons/project/__openerp__.py @@ -29,7 +29,7 @@ "sequence": 8, 'complexity': "easy", "images": ["images/gantt.png", "images/project_dashboard.jpeg","images/project_task_tree.jpeg","images/project_task.jpeg","images/project.jpeg","images/task_analysis.jpeg"], - "depends": ["base_setup", "product", "analytic", "board", "mail", "resource"], + "depends": ["base_setup", "product", "analytic", "board", "mail", "resource","web_kanban"], "description": """ Project management module tracks multi-level projects, tasks, work done on tasks, eso. ====================================================================================== @@ -70,6 +70,8 @@ Dashboard for project members that includes: 'installable': True, 'auto_install': False, 'application': True, + 'css': ['static/src/css/project.css'], + 'js': ['static/src/js/dropdown.js','static/src/js/project.js'], 'certificate': '0075116868317', } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/project/project.py b/addons/project/project.py index 0335b7cb540..a25e55d12e5 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -155,6 +155,13 @@ class project(osv.osv): raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.')) return super(project, self).unlink(cr, uid, ids, *args, **kwargs) + def _task_count(self, cr, uid, ids, field_name, arg, context=None): + res = dict.fromkeys(ids, 0) + task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)]) + for task in self.pool.get('project.task').browse(cr, uid, task_ids, context): + res[task.project_id.id] += 1 + return res + _columns = { 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250), 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."), @@ -191,7 +198,15 @@ class project(osv.osv): 'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), + 'use_tasks': fields.boolean('Use Tasks', help="Check this field if this project is aimed at managing tasks"), + 'task_count': fields.function(_task_count, type='integer', string="Open Tasks"), + 'color': fields.integer('Color Index'), + 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'), } + + def dummy(self, cr, uid, ids, context): + return True + def _get_type_common(self, cr, uid, context): ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context) return ids @@ -201,7 +216,8 @@ class project(osv.osv): 'active': True, 'priority': 1, 'sequence': 10, - 'type_ids': _get_type_common + 'type_ids': _get_type_common, + 'use_tasks': True, } # TODO: Why not using a SQL contraints ? diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index 956b159a0bb..19a1d0d4454 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -43,12 +43,17 @@ + + + + - + + @@ -155,6 +160,94 @@ + + + + + + project.project.kanban + project.project + kanban + + + + + + + + + + + + + +
+
  • + B + +

    ...

    + +
    + +
  • +
    + +
    +
    +
    +
    +
    project.project.gantt @@ -171,8 +264,8 @@ project.project form [] - tree,form,gantt - + kanban,tree,form,gantt + {} A project contains a set of tasks or issues that will be performed by your resources assigned to it. A project can be hierarchically structured, as a child of a Parent Project. This allows you to design large project structures with different phases spread over the project duration cycle. Each user can set his default project in his own preferences to automatically filter the tasks or issues he usually works on. If you choose to invoice the time spent on a project task, you can find project tasks to be invoiced in the billing section. @@ -314,8 +407,7 @@ - - + project.task.kanban @@ -627,22 +719,13 @@ - + - + - - res.company.task.config res.company diff --git a/addons/project/static/src/css/project.css b/addons/project/static/src/css/project.css new file mode 100644 index 00000000000..93437c878ba --- /dev/null +++ b/addons/project/static/src/css/project.css @@ -0,0 +1,220 @@ +.project_fields { + margin-top: 1px; + margin-bottom: 1px; + font-size: 11px; + padding-left: 0px; + } +.project_fields td { + border: none; + padding: 2px 0 2px 8px; } +.project_fields th { + padding: 0; + border-right: 1px solid #dddddd; + vertical-align: top; + margin-right: 8px; } +.project_vignettes{ + padding: 4px !important } + .project_vignettes li { + float: left; + } + .project_vignettes .project_avatar { + width: 30px; + height: 30px; + padding-left: 0px; } + .project_vignettes .project_fields { + width: 100%; } + .project_vignettes .project_fields th { + width: 120px; + font-weight: normal; } + .project_vignettes .project_fields td { + color: #888888; } + .project_vignettes h4 a { + color: #4c4c4c; } + .project_vignettes > li h4 { + margin-bottom: 2px; + padding-left: 2px; } + +.oe_project_buttons { + padding: 2px 2px !important; + background: none !important; + background-color: transparent !important; + border: hidden !important; + color: #8A89BA !important; +} +.oe_project_buttons:hover { + cursor: pointer; + color: #0000FF; +} +.project_avatar { + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + } +.click_button { + background: none !important; + background-color: transparent !important; + border: hidden !important; + min-height: 155px; + min-width: 265px; + border: hidden; + /*margin-left: 6px !important;*/ + margin-top: 3px !important; + text-align: left; + vertical-align: super; + font-size: 11px; + -webkit-box-align: baseline; +} + + +.click_button:hover { + cursor: default !important; +} +.dropdown-menu { + display: none; + position: absolute; } +.dropdown { + position: relative; } +.dropdown-toggle:after { + width: 0; + height: 0; + display: inline-block; + content: "&darr"; + text-indent: -99999px; + vertical-align: top; + margin-top: 8px; + margin-left: 4px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid white; + filter: alpha(opacity=50); + -khtml-opacity: 0.5; + -moz-opacity: 0.5; + opacity: 0.5; } + .oe_project_kanban_vignette .dropdown-menu .color-chooser { + padding: 0 3px; } + .oe_project_kanban_vignette .dropdown-menu .color-chooser li { + float: left; } + .oe_project_kanban_vignette .dropdown-menu .color-chooser li a { + padding: 2px; } +a.oe_project_kanban_action { + position: absolute; + right: 0; + display: none; + } + a.oe_project_kanban_action:hover { + text-decoration: none; } + a.oe_project_kanban_action .oe_i { + color: #4c4c4c; } + +.square { + display: inline-block; + width: 18px; + height: 18px; + border:1px solid grey; + } + +.oe_kanban_color_0 { + background: white; +} +.oe_kanban_color_1 { + background: #B1DCFE; +} +.oe_kanban_color_2 { + background: #FF8E8E; +} +.oe_kanban_color_3 { + background: khaki; +} +.oe_kanban_color_4 { + background: thistle; +} +.oe_kanban_color_5 { + background: orange; +} + +.open { + display: block; + } + .open .dropdown-menu { + display: block; } + +a.oe_project_kanban_action { + position: absolute; + right: 0; + } + a.oe_project_kanban_action:hover { + text-decoration: none; + } + a.oe_project_kanban_action .eo_i { + color: #4c4c4c; } + +.oe_project_kanban_vignette { + position: relative; + min-height: 50px; + /*background: white;*/ + border: 1px solid #d8d8d8; + border-bottom-color: #b9b9b9; + padding: 6px; + margin: 6px 0; + display: inline-block; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + +} + .oe_project_kanban_vignette:last-child { + margin-bottom: 0; } + .oe_project_kanban_vignette:hover { + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.6); + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.6); + -box-shadow: 0 0 3px rgba(0, 0, 0, 0.6); } + .oe_project_kanban_vignette h4 { + margin: 0 0 2px; } + +.oe_project_kanban_vignette .dropdown-menu { + top: 30px; + right: -140px; + padding: 4px; + border: 1px solid #afafb6; + width: 160px; + overflow-x: hidden; + z-index: 900; + background: white; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); + -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); } + .oe_project_kanban_vignette .dropdown-menu p { + margin-left: 12px; } + .oe_project_kanban_vignette .dropdown-menu li { + float: none; + display: block; + background-color: none; } + .oe_project_kanban_vignette .dropdown-menu li a { + display: block; + padding: 3px 6px; + clear: both; + font-weight: normal; + line-height: 14px; + color: #4c4c4c; + text-decoration: none; } + .oe_project_kanban_vignette .dropdown-menu li a:hover { + background: #f0f0fa; + background: -moz-linear-gradient(#f0f0fa, #eeeef6); + background: -webkit-gradient(linear, left top, left bottom, from(#f0f0fa), to(#eeeef6)); + background: -webkit-linear-gradient(#f0f0fa, #eeeef6); + -moz-box-shadow: none; + -webkit-box-shadow: none; + -box-shadow: none; } + +.oe_project_kanban_vignette .dropdown-menu .color-chooser { + padding: 0 3px; } + .oe_project_kanban_vignette .dropdown-menu .color-chooser li { + float: left; } + .oe_project_kanban_vignette .dropdown-menu .color-chooser li a { + padding: 2px; } diff --git a/addons/project/static/src/img/tasks_icon.png b/addons/project/static/src/img/tasks_icon.png new file mode 100644 index 00000000000..5c5073b1633 Binary files /dev/null and b/addons/project/static/src/img/tasks_icon.png differ diff --git a/addons/project/static/src/js/dropdown.js b/addons/project/static/src/js/dropdown.js new file mode 100644 index 00000000000..54b61c5e9d0 --- /dev/null +++ b/addons/project/static/src/js/dropdown.js @@ -0,0 +1,92 @@ +/* ============================================================ + * bootstrap-dropdown.js v2.0.2 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function( $ ){ + + "use strict" + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle="dropdown"]' + , Dropdown = function ( element ) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function ( e ) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + , isActive + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + isActive = $parent.hasClass('open') + + clearMenus() + !isActive && $parent.toggleClass('open') + + return false + } + + } + + function clearMenus() { + $(toggle).parent().removeClass('open') + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html').on('click.dropdown.data-api', clearMenus) + $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) + }) + +}( window.jQuery ); \ No newline at end of file diff --git a/addons/project/static/src/js/project.js b/addons/project/static/src/js/project.js new file mode 100644 index 00000000000..09a7c9eb256 --- /dev/null +++ b/addons/project/static/src/js/project.js @@ -0,0 +1,48 @@ +openerp.project = function(openerp) { + openerp.web_kanban.ProjectKanban = openerp.web_kanban.KanbanRecord.include({ + bind_events: function() { + var self = this; + self._super(); + if(this.view.dataset.model == 'project.project') { + + /*set avatar title for members. + In many2many fields, returns only list of ids. + we can implement return value of m2m fields like [(1,"Adminstration"),...]. + */ + _.each($(this.$element).find('.project_avatar'),function(avatar){ + var dataset = new openerp.web.DataSetSearch(this, 'res.users', self.session.context, [['id','=',avatar.id]]); + dataset.read_slice(['name']).then(function(result){ + avatar.setAttribute("title",result[0].name) + }); + }); + + // set sequence like Tasks,Issues,Timesheets and Phases + var my_list = $("#list a") + my_list.sort(function (a, b) { + var aValue = parseInt(a.id); + var bValue = parseInt(b.id); + return aValue == bValue ? 0 : aValue < bValue ? -1 : 1; + }); + $('#list').replaceWith(my_list); + + // when vignette is clicked, it opens the first action in sequence + if (my_list.length != 0) { + var click_button = $(this.$element).find('.click_button') + click_button.attr('data-name', my_list[0].getAttribute('data-name')); + click_button.attr('data-type', "action"); + } + + /* set background color. + we can do other way to implement new widget. + because we need to rpc call for that. + */ + this.$element.find('.bgcolor').click(function(){ + var color = parseInt($(this).find('span').attr('class').split(' ')[0].substring(16)) + var color_class = $(this).find('span').attr('class').split(' ')[0] + $(this).closest('#oe_project_kanban_vignette').removeClass().addClass(color_class + ' oe_project_kanban_vignette'); + self.view.dataset.write(parseInt(this.id), {color:color}); + }); + } + } + }); +} diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index e61af235d42..5a792ac4d28 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -222,6 +222,8 @@ class project_issue(crm.crm_case, osv.osv): }), } + def on_change_project(self, cr, uid, ids, project_id, context=None): + return {} _defaults = { 'active': 1, @@ -506,9 +508,23 @@ project_issue() class project(osv.osv): _inherit = "project.project" + + def _issue_count(self, cr, uid, ids, field_name, arg, context=None): + res = dict.fromkeys(ids, 0) + issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)]) + for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context): + res[issue.project_id.id] += 1 + return res + _columns = { 'project_escalation_id' : fields.many2one('project.project','Project Escalation', help='If any issue is escalated from the current Project, it will be listed under the project selected here.', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), - 'reply_to' : fields.char('Reply-To Email Address', size=256) + 'reply_to' : fields.char('Reply-To Email Address', size=256), + 'use_issues' : fields.boolean('Use Issues', help="Check this field if this project manages issues"), + 'issue_count': fields.function(_issue_count, type='integer'), + } + + _defaults = { + 'use_issues': True, } def _check_escalation(self, cr, uid, ids, context=None): diff --git a/addons/project_issue/project_issue_menu.xml b/addons/project_issue/project_issue_menu.xml index dd61992a555..1c4cecfe439 100644 --- a/addons/project_issue/project_issue_menu.xml +++ b/addons/project_issue/project_issue_menu.xml @@ -45,15 +45,6 @@ - - diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index 7c22ad748c0..35a256be17e 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -334,6 +334,14 @@ + + # ------------------------------------------------------ # Project @@ -345,12 +353,32 @@ form + + + - + + + project.project.kanban.inherited + project.project + kanban + + + + + + + + + Issues() + + + diff --git a/addons/project_long_term/project_long_term.py b/addons/project_long_term/project_long_term.py index 612bd784bb0..925e4258ed9 100644 --- a/addons/project_long_term/project_long_term.py +++ b/addons/project_long_term/project_long_term.py @@ -215,9 +215,23 @@ project_user_allocation() class project(osv.osv): _inherit = "project.project" + + def _phase_count(self, cr, uid, ids, field_name, arg, context=None): + res = dict.fromkeys(ids, 0) + phase_ids = self.pool.get('project.phase').search(cr, uid, [('project_id', 'in', ids)]) + for phase in self.pool.get('project.phase').browse(cr, uid, phase_ids, context): + res[phase.project_id.id] += 1 + return res + _columns = { 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"), + 'use_phases': fields.boolean('Use Phases', help="Check this field if project manages phases"), + 'phase_count': fields.function(_phase_count, type='integer', string="Open Phases"), } + _defaults = { + 'use_phases': True, + } + def schedule_phases(self, cr, uid, ids, context=None): context = context or {} if type(ids) in (long, int,): diff --git a/addons/project_long_term/project_long_term_view.xml b/addons/project_long_term/project_long_term_view.xml index 7231395a2a9..b4042fc42e7 100644 --- a/addons/project_long_term/project_long_term_view.xml +++ b/addons/project_long_term/project_long_term_view.xml @@ -84,12 +84,12 @@ - + Planning of Users project.user.allocation form gantt,tree,form,calendar - {} + {} @@ -98,6 +98,44 @@ # Project Phase # ------------------------------------------------------ + + Inherit project form : Phase + project.project + form + + + + + + + + + + + + project.project.kanban.inherited + project.project + kanban + + + + + + + + Phases() + + + + project.phase.form project.phase @@ -278,7 +316,7 @@ - + @@ -327,16 +365,6 @@ view_mode="tree,form" /> - - # ------------------------------------------------------ # Menu Items # ------------------------------------------------------ diff --git a/addons/project_timesheet/project_timesheet.py b/addons/project_timesheet/project_timesheet.py index db7066505e1..585b3cd6ec8 100644 --- a/addons/project_timesheet/project_timesheet.py +++ b/addons/project_timesheet/project_timesheet.py @@ -29,6 +29,36 @@ from tools.translate import _ class project_project(osv.osv): _inherit = 'project.project' + def _to_invoice(self, cr, uid, ids, field_name, arg, context=None): + account_analytic_line = self.pool.get("account.analytic.line") + res = {} + for project in self.browse(cr,uid,ids,context=context): + line_ids = account_analytic_line.search(cr, uid, [('account_id', '=', project.analytic_account_id.id), ('to_invoice','=',1), ('invoice_id','=',False)]) + lines = account_analytic_line.browse(cr, uid, line_ids, context) + res[project.id] = { + 'amount_to_invoice': sum(line.amount for line in lines), + 'time_to_invoice': sum(line.unit_amount for line in lines), + } + return res + + def _timesheet_count(self, cr, uid, ids, field_name, arg, context=None): + account_analytic_line = self.pool.get('account.analytic.line') + res = {} + for project in self.browse(cr, uid, ids, context): + line_ids = account_analytic_line.search(cr, uid, [('account_id', '=', project.analytic_account_id.id)]) + res[project.id] = len(line_ids) + return res + + _columns = { + 'use_timesheets': fields.boolean('Timesheets', help="Check this field if this project manages timesheets"), + 'amount_to_invoice': fields.function(_to_invoice, string="Amount to Invoice", multi="sums"), + 'time_to_invoice': fields.function(_to_invoice, string="Time to Invoice", multi="sums"), + 'timesheet_count': fields.function(_timesheet_count, type='integer', string="Issue"), + } + _defaults = { + 'use_timesheets': True, + } + def onchange_partner_id(self, cr, uid, ids, part=False, context=None): res = super(project_project, self).onchange_partner_id(cr, uid, ids, part, context) if part and res and ('value' in res): @@ -39,6 +69,28 @@ class project_project(osv.osv): factor_id = data_obj.browse(cr, uid, data_id).res_id res['value'].update({'to_invoice': factor_id}) return res + + def open_timesheets(self, cr, uid, ids, context=None): + """ open Timesheets view """ + project = self.browse(cr, uid, ids[0], context) + try: + journal_id = self.pool.get('ir.model.data').get_object(cr, uid, 'hr_timesheet', 'analytic_journal').id + except ValueError: + journal_id = False + view_context = { + 'search_default_account_id': [project.analytic_account_id.id], + 'default_account_id': project.analytic_account_id.id, + 'default_journal_id': journal_id, + } + return { + 'type': 'ir.actions.act_window', + 'name': _('Bill Tasks Works'), + 'res_model': 'account.analytic.line', + 'view_type': 'form', + 'view_mode': 'tree,form', + 'context': view_context, + 'nodestroy': True, + } project_project() diff --git a/addons/project_timesheet/project_timesheet_view.xml b/addons/project_timesheet/project_timesheet_view.xml index 0c9d6c57f44..f0b2e2abc20 100644 --- a/addons/project_timesheet/project_timesheet_view.xml +++ b/addons/project_timesheet/project_timesheet_view.xml @@ -7,6 +7,11 @@ form + + + + + @@ -19,6 +24,41 @@ + + project.project.kanban.inherited + project.project + kanban + + + + + + + + + + Timesheets() + + + + + Amount to invoice + + + + + + Time to Invoice + + + + + + + + + Inherit project form : Customer project.project