diff options
46 files changed, 895 insertions, 1253 deletions
diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index 49e1245c408..04b234d6df8 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -642,7 +642,7 @@ class account_analytic_account(osv.osv): return True def hr_to_invoice_timesheets(self, cr, uid, ids, context=None): - domain = [('invoice_id','=',False),('to_invoice','!=',False), ('journal_id.type', '=', 'general'), ('account_id', 'in', ids)] + domain = [('invoice_id','=',False),('to_invoice','!=',False), ('journal_id.type', '=', 'general'), ('account_id', 'in', ids), ('unit_amount', '>', 0)] names = [record.name for record in self.browse(cr, uid, ids, context=context)] name = _('Timesheets to Invoice of %s') % ','.join(names) return { diff --git a/addons/account_analytic_analysis/account_analytic_analysis_view.xml b/addons/account_analytic_analysis/account_analytic_analysis_view.xml index c35dce559df..16bf45c1570 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis_view.xml +++ b/addons/account_analytic_analysis/account_analytic_analysis_view.xml @@ -83,18 +83,18 @@ context="{'default_partner_id': [partner_id], 'search_default_project_id': [active_id],'default_project_id': [active_id], 'default_pricelist_id': pricelist_id}" /> </td> - </tr><tr> + </tr><tr attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"> <td class="oe_timesheet_grey"> <label string="On Timesheets"/> </td><td class="oe_timesheet_grey"> </td><td> - <field class="oe_inline" name="hours_qtt_est" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/> + <field class="oe_inline" name="hours_qtt_est"/> </td><td> - <field class="oe_inline" name="timesheet_ca_invoiced" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/> + <field class="oe_inline" name="timesheet_ca_invoiced"/> </td><td> - <field class="oe_inline" name="remaining_hours_to_invoice" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/> + <field class="oe_inline" name="remaining_hours_to_invoice"/> </td><td> - <field class="oe_inline" name="ca_to_invoice" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/> + <field class="oe_inline" name="ca_to_invoice"/> </td><td class="oe_timesheet_action" attrs="{'invisible': ['|',('invoice_on_timesheets','=',False),('type','=','template')]}"> <span attrs="{'invisible': [('ca_to_invoice','=',0.0)]}" class="oe_grey"> <button name="hr_to_invoice_timesheets" diff --git a/addons/account_analytic_analysis/analytic_account_demo.xml b/addons/account_analytic_analysis/analytic_account_demo.xml index 85f4e821d9a..b4fb3c58168 100644 --- a/addons/account_analytic_analysis/analytic_account_demo.xml +++ b/addons/account_analytic_analysis/analytic_account_demo.xml @@ -15,7 +15,7 @@ <field name="project_id" ref="analytic.analytic_consultancy"/> </record> - <record id="hr_timesheet.working_hours_maintenance" model="hr.analytic.timesheet"> + <record id="hr_timesheet.working_hours_maintenance" model="account.analytic.line"> <field name="account_id" ref="analytic.analytic_consultancy"/> <field name="to_invoice" ref="hr_timesheet_invoice.timesheet_invoice_factor1"/> <field name="unit_amount">100</field> diff --git a/addons/analytic/models/analytic.py b/addons/analytic/models/analytic.py index 7ff3a74e33b..88d4e4f56ec 100644 --- a/addons/analytic/models/analytic.py +++ b/addons/analytic/models/analytic.py @@ -357,7 +357,8 @@ class account_analytic_line(osv.osv): _defaults = { 'date': __get_default_date, 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c), - 'amount': 0.00 + 'amount': 0.00, + 'user_id': lambda self, cr, uid, ctx: ctx.get('user_id') or uid, } _order = 'date desc' diff --git a/addons/analytic_user_function/analytic_user_function.py b/addons/analytic_user_function/analytic_user_function.py index 855ee4ced47..bbcff141a47 100644 --- a/addons/analytic_user_function/analytic_user_function.py +++ b/addons/analytic_user_function/analytic_user_function.py @@ -19,22 +19,24 @@ # ############################################################################## -from openerp.osv import fields,osv +from openerp.osv import fields, osv from openerp.tools.translate import _ import openerp.addons.decimal_precision as dp from openerp.exceptions import UserError + class analytic_user_funct_grid(osv.osv): - _name="analytic.user.funct.grid" - _description= "Price per User" - _rec_name="user_id" - _columns={ + _name = "analytic.user.funct.grid" + _description = "Price per User" + _rec_name = "user_id" + _columns = { 'user_id': fields.many2one("res.users", "User", required=True,), 'product_id': fields.many2one("product.product", "Service", required=True,), 'account_id': fields.many2one("account.analytic.account", "Analytic Account", required=True,), 'uom_id': fields.related("product_id", "uom_id", relation="product.uom", string="Unit of Measure", type="many2one", readonly=True), 'price': fields.float('Price', digits_compute=dp.get_precision('Product Price'), help="Price per hour for this user.", required=True), } + def onchange_user_product_id(self, cr, uid, ids, user_id, product_id, context=None): if not user_id: return {} @@ -46,7 +48,7 @@ class analytic_user_funct_grid(osv.osv): value = {} prod = False if product_id: - prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context) + prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context) emp = emp_obj.browse(cr, uid, emp_id[0], context=context) if emp.product_id and not product_id: value['product_id'] = emp.product_id.id @@ -64,92 +66,73 @@ class account_analytic_account(osv.osv): } -class hr_analytic_timesheet(osv.osv): - _inherit = "hr.analytic.timesheet" +class account_analytic_line(osv.osv): + _inherit = "account.analytic.line" + # Look in account, if no value for the user => look in parent until there is no more parent to look # Take the first found... if nothing found => return False - def _get_related_user_account_recursiv(self, cr, uid, user_id, account_id): - temp=self.pool.get('analytic.user.funct.grid').search(cr, uid, [('user_id', '=', user_id),('account_id', '=', account_id) ]) - account=self.pool.get('account.analytic.account').browse(cr, uid, account_id) - if temp: - return temp + def _get_related_user_account_recursiv(self, cr, uid, user_id, account_id, context): + user_function_id = self.pool.get('analytic.user.funct.grid').search(cr, uid, [('user_id', '=', user_id), ('account_id', '=', account_id)]) + account = self.pool['account.analytic.account'].browse(cr, uid, account_id) + if user_function_id: + return user_function_id + elif account.parent_id: + return self._get_related_user_account_recursiv(cr, uid, user_id, account.parent_id.id, context) else: - if account.parent_id: - return self._get_related_user_account_recursiv(cr, uid, user_id, account.parent_id.id) - else: - return False + return False - def on_change_account_id(self, cr, uid, ids, account_id, user_id=False, unit_amount=0): + def on_change_account_id(self, cr, uid, ids, account_id, user_id=False, unit_amount=0, is_timesheet=False, context=None): res = {} if not (account_id): - #avoid a useless call to super return res - if not (user_id): - return super(hr_analytic_timesheet, self).on_change_account_id(cr, uid, ids, account_id) + return super(account_analytic_line, self).on_change_account_id(cr, uid, ids, account_id, user_id, is_timesheet, context) - #get the browse record related to user_id and account_id - temp = self._get_related_user_account_recursiv(cr, uid, user_id, account_id) - if not temp: - #if there isn't any record for this user_id and account_id - return super(hr_analytic_timesheet, self).on_change_account_id(cr, uid, ids, account_id) + # get the browse record related to user_id and account_id + user_function_id = self._get_related_user_account_recursiv(cr, uid, user_id, account_id, context) + if not user_function_id: + # if there isn't any record for this user_id and account_id + return super(account_analytic_line, self).on_change_account_id(cr, uid, ids, account_id, user_id, is_timesheet, context) else: - #get the old values from super and add the value from the new relation analytic_user_funct_grid - r = self.pool.get('analytic.user.funct.grid').browse(cr, uid, temp)[0] - res.setdefault('value',{}) - res['value']= super(hr_analytic_timesheet, self).on_change_account_id(cr, uid, ids, account_id)['value'] - res['value']['product_id'] = r.product_id.id - res['value']['product_uom_id'] = r.product_id.uom_id.id - - #the change of product has to impact the amount, uom and general_account_id - a = r.product_id.property_account_expense.id - if not a: - a = r.product_id.categ_id.property_account_expense_categ.id - if not a: - raise UserError(_('There is no expense account defined for this product: "%s" (id:%d)') % (r.product_id.name, r.product_id.id,)) - # Compute based on pricetype - if unit_amount: - amount_unit = self.on_change_unit_amount(cr, uid, ids, - r.product_id.id, unit_amount, False, r.product_id.uom_id.id)['value']['amount'] - amount = unit_amount * amount_unit - res ['value']['amount']= - round(amount, 2) - res ['value']['general_account_id']= a + # get the old values from super and add the value from the new relation analytic_user_funct_grid + res['value'] = super(account_analytic_line, self).on_change_account_id(cr, uid, ids, account_id, user_id, is_timesheet, context)['value'] + res['value'].update(self._get_values_based_on_user_function(cr, uid, ids, user_function_id, unit_amount, context=context)) return res - def on_change_user_id(self, cr, uid, ids, user_id, account_id, unit_amount=0): - res = super(hr_analytic_timesheet, self).on_change_user_id(cr, uid, ids, user_id) + def on_change_user_id(self, cr, uid, ids, user_id, unit_amount=0, account_id=None, is_timesheet=False, context=None): + res = {} + res = super(account_analytic_line, self).on_change_user_id(cr, uid, ids, user_id, is_timesheet, context=context) if account_id: - #get the browse record related to user_id and account_id - temp = self._get_related_user_account_recursiv(cr, uid, user_id, account_id) - if temp: - #add the value from the new relation analytic_user_funct_grid - r = self.pool.get('analytic.user.funct.grid').browse(cr, uid, temp)[0] - res['value']['product_id'] = r.product_id.id - - #the change of product has to impact the amount, uom and general_account_id - a = r.product_id.property_account_expense.id - if not a: - a = r.product_id.categ_id.property_account_expense_categ.id - if not a: - raise UserError( - _('There is no expense account defined ' \ - 'for this product: "%s" (id:%d)') % \ - (r.product_id.name, r.product_id.id,)) - # Compute based on pricetype - if unit_amount: - amount_unit = self.on_change_unit_amount(cr, uid, ids, - r.product_id.id, unit_amount, False, r.product_id.uom_id.id)['value']['amount'] - - amount = unit_amount * amount_unit - res ['value']['amount']= - round(amount, 2) - res ['value']['general_account_id']= a + # get the browse record related to user_id and account_id + user_function_id = self._get_related_user_account_recursiv(cr, uid, user_id, account_id, context) + if user_function_id: + res['value'].update(self._get_values_based_on_user_function(cr, uid, ids, user_function_id, unit_amount, context=context)) return res -class account_analytic_line(osv.osv): - _inherit = "account.analytic.line" - def _get_invoice_price(self, cr, uid, account, product_id, user_id, qty, context = {}): + # Returns appropriate values for an analytic line if the user has a specific user_function + def _get_values_based_on_user_function(self, cr, uid, ids, user_function_id, unit_amount=0, context=None): + res = {} + user_function = self.pool['analytic.user.funct.grid'].browse(cr, uid, user_function_id, context=context) + product = user_function.product_id + res['product_id'] = product.id + res['uom_id'] = product.uom_id.id + + expense_account = product.property_account_expense.id + if not expense_account: + expense_account = product.categ_id.property_account_expense_categ.id + if not expense_account: + raise UserError(_('There is no expense account defined for this product: "%s" (id:%d)') % (product.name, product.id,)) + res['general_account_id'] = expense_account + + if unit_amount: + new_amount = self.on_change_unit_amount(cr, uid, ids, product.id, unit_amount, False, product.uom_id.id) + res['amount'] = new_amount['value']['amount'] + + return res + + def _get_invoice_price(self, cr, uid, account, product_id, user_id, qty, context={}): for grid in account.user_product_ids: - if grid.user_id.id==user_id: + if grid.user_id.id == user_id: return grid.price return super(account_analytic_line, self)._get_invoice_price(cr, uid, account, product_id, user_id, qty, context) diff --git a/addons/analytic_user_function/analytic_user_function_view.xml b/addons/analytic_user_function/analytic_user_function_view.xml index 132f44ebef5..6010970b0ce 100644 --- a/addons/analytic_user_function/analytic_user_function_view.xml +++ b/addons/analytic_user_function/analytic_user_function_view.xml @@ -66,7 +66,7 @@ <field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_form"/> <field name="arch" type="xml"> <xpath expr="//field[@name='timesheet_ids']/tree/field[@name='account_id']" position="attributes"> - <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount)</attribute> + <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount, is_timesheet, context)</attribute> </xpath> </field> </record> @@ -79,8 +79,7 @@ <field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_form"/> <field name="arch" type="xml"> <xpath expr="//field[@name='timesheet_ids']/form/field[@name='account_id']" position="attributes"> - <attribute name="domain">[('type','=','normal'),('state', '<>', 'close')]</attribute> - <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount)</attribute> + <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount, is_timesheet, context)</attribute> <attribute name="context">{'default_invoice_on_timesheets': 1}</attribute> </xpath> </field> @@ -88,50 +87,39 @@ <!-- hr.analytic.timesheet inherited views --> <record model="ir.ui.view" id="hr_timesheet_line_form_inherit"> - <field name="name">hr.analytic.timesheet.form</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/> + <field name="name">account.analytic.line.form</field> + <field name="model">account.analytic.line</field> + <field name="inherit_id" ref="hr_timesheet_invoice.hr_timesheet_line_form"/> <field name="arch" type="xml"> <xpath expr="//field[@name='account_id']" position="attributes"> - <attribute name="domain">[('type','=','normal'),('state', '<>', 'close')]</attribute> - <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount)</attribute> + <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount, is_timesheet, context)</attribute> </xpath> </field> </record> <record model="ir.ui.view" id="hr_timesheet_line_form_inherit_2"> - <field name="name">hr.analytic.timesheet.form</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/> + <field name="name">account.analytic.line.form</field> + <field name="model">account.analytic.line</field> + <field name="inherit_id" ref="hr_timesheet_invoice.hr_timesheet_line_form"/> <field name="arch" type="xml"> <xpath expr="//field[@name='user_id']" position="replace"> - <field name="user_id" required="1" on_change="on_change_user_id(user_id, account_id, unit_amount)"/> + <field name="user_id" required="1" on_change="on_change_user_id(user_id, unit_amount, account_id, is_timesheet, context)"/> </xpath> </field> </record> <record model="ir.ui.view" id="hr_timesheet_line_tree_inherit"> - <field name="name">hr.analytic.timesheet.tree</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/> + <field name="name">account.analytic.line.tree</field> + <field name="model">account.analytic.line</field> + <field name="inherit_id" ref="hr_timesheet_invoice.hr_timesheet_line_tree"/> <field name="arch" type="xml"> <xpath expr="/tree/field[@name='account_id']" position="attributes"> - <attribute name="domain">[('type','=','normal'),('state', '<>', 'close')]</attribute> - <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount)</attribute> + <attribute name="on_change">on_change_account_id(account_id, user_id, unit_amount, is_timesheet, context)</attribute> </xpath> - </field> - </record> - - <record model="ir.ui.view" id="hr_timesheet_line_tree_inherit_2"> - <field name="name">hr.analytic.timesheet.tree</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/> - <field name="arch" type="xml"> <xpath expr="/tree/field[@name='user_id']" position="replace"> - <field name="user_id" required="1" on_change="on_change_user_id(user_id, account_id, unit_amount)" context="{'default_groups_ref': ['base.group_user']}"/> + <field name="user_id" required="1" on_change="on_change_user_id(user_id, unit_amount, account_id, is_timesheet, context)" context="{'default_groups_ref': ['base.group_user']}"/> </xpath> </field> </record> - </data> </openerp> diff --git a/addons/hr_timesheet/__openerp__.py b/addons/hr_timesheet/__openerp__.py index ff9ebb91d93..61e9e13be0a 100644 --- a/addons/hr_timesheet/__openerp__.py +++ b/addons/hr_timesheet/__openerp__.py @@ -43,7 +43,6 @@ up a management by affair. 'depends': ['account', 'hr', 'base', 'hr_attendance'], 'data': [ 'security/ir.model.access.csv', - 'security/hr_timesheet_security.xml', 'hr_timesheet_view.xml', 'report/hr_timesheet_report_view.xml', 'hr_timesheet_installer.xml', @@ -52,7 +51,6 @@ up a management by affair. 'demo': ['hr_timesheet_demo.xml'], 'test': [ 'test/hr_timesheet_users.yml', - 'test/hr_timesheet_demo.yml', ], 'installable': True, 'auto_install': False, diff --git a/addons/hr_timesheet/hr_timesheet.py b/addons/hr_timesheet/hr_timesheet.py index fcd50b50393..3e2aec8b621 100644 --- a/addons/hr_timesheet/hr_timesheet.py +++ b/addons/hr_timesheet/hr_timesheet.py @@ -19,79 +19,121 @@ # ############################################################################## -import time +from openerp import models, api, fields, exceptions -from openerp.osv import fields -from openerp.osv import osv from openerp.tools.translate import _ -import openerp -from openerp.exceptions import UserError -class hr_employee(osv.osv): - _name = "hr.employee" + +class HrEmployee(models.Model): _inherit = "hr.employee" - _columns = { - 'product_id': fields.many2one('product.product', 'Product', help="If you want to reinvoice working time of employees, link this employee to a service to determinate the cost price of the job."), - 'journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'), - 'uom_id': fields.related('product_id', 'uom_id', type='many2one', relation='product.uom', string='Unit of Measure', store=True, readonly=True) - } - - def _getAnalyticJournal(self, cr, uid, context=None): - md = self.pool.get('ir.model.data') - try: - dummy, res_id = md.get_object_reference(cr, uid, 'hr_timesheet', 'analytic_journal') - #search on id found in result to check if current user has read access right - check_right = self.pool.get('account.analytic.journal').search(cr, uid, [('id', '=', res_id)], context=context) - if check_right: - return res_id - except ValueError: - pass - return False - def _getEmployeeProduct(self, cr, uid, context=None): - md = self.pool.get('ir.model.data') - try: - dummy, res_id = md.get_object_reference(cr, uid, 'product', 'product_product_consultant') - #search on id found in result to check if current user has read access right - check_right = self.pool.get('product.template').search(cr, uid, [('id', '=', res_id)], context=context) - if check_right: - return res_id - except ValueError: - pass - return False - - _defaults = { - 'journal_id': _getAnalyticJournal, - 'product_id': _getEmployeeProduct - } - - -class hr_analytic_timesheet(osv.osv): - _name = "hr.analytic.timesheet" - _table = 'hr_analytic_timesheet' - _description = "Timesheet Line" - _inherits = {'account.analytic.line': 'line_id'} - _order = "id desc" - _columns = { - 'line_id': fields.many2one('account.analytic.line', 'Analytic Line', ondelete='cascade', required=True), - 'partner_id': fields.related('account_id', 'partner_id', type='many2one', string='Partner', relation='res.partner', store=True), - } - - def unlink(self, cr, uid, ids, context=None): - toremove = {} - for obj in self.browse(cr, uid, ids, context=context): - toremove[obj.line_id.id] = True - super(hr_analytic_timesheet, self).unlink(cr, uid, ids, context=context) - self.pool.get('account.analytic.line').unlink(cr, uid, toremove.keys(), context=context) - return True + @api.model + def _get_default_analytic_journal(self): + # We need the "or ..." because we need an empty recordset of the right type, 'False' is not enough. + return self.env.ref('hr_timesheet.analytic_journal', raise_if_not_found=False) or self.env['account.analytic.journal'] + + @api.model + def _get_default_employee_product(self): + # We need the "or ..." because we need an empty recordset of the right type, 'False' is not enough. + return self.env.ref('product.product_product_consultant', raise_if_not_found=False) or self.env['account.analytic.journal'] + + product_id = fields.Many2one('product.product', 'Product', help="If you want to reinvoice working time of employees, link this employee to a service to determinate the cost price of the job.", default=_get_default_employee_product) + journal_id = fields.Many2one('account.analytic.journal', 'Analytic Journal', default=_get_default_analytic_journal) + uom_id = fields.Many2one('product.uom', related='product_id.uom_id', string='Unit of Measure', store=True, readonly=True) + + +class account_analytic_line(models.Model): + _inherit = 'account.analytic.line' + + def default_get(self, cr, uid, fields, context=None): + values = super(account_analytic_line, self).default_get(cr, uid, fields, context=context) + if values.get('is_timesheet'): + if 'product_id' in fields: + values['product_id'] = self._get_employee_product(cr, uid, context=context) + if 'product_uom_id' in fields: + values['product_uom_id'] = self._get_employee_unit(cr, uid, context=context) + if 'general_account_id' in fields: + values['general_account_id'] = self._get_general_account(cr, uid, context=context) + if 'journal_id' in fields: + values['journal_id'] = self._get_analytic_journal(cr, uid, context=context) + return values + + is_timesheet = fields.Boolean() + + @api.model + def _get_employee_product(self, user_id=None): + emp = self.env['hr.employee'].search([('user_id', '=', user_id or self.env.uid)], limit=1) + if emp and emp.product_id.id: + return emp.product_id.id + else: + raise exceptions.ValidationError("This user or employee is not associated to a valid Product") + @api.model + def _get_employee_unit(self, user_id=None): + emp = self.env['hr.employee'].search([('user_id', '=', user_id or self.env.uid)], limit=1) + if emp and emp.product_id.uom_id.id: + return emp.product_id.uom_id.id + else: + raise exceptions.ValidationError("This user or employee is not associated to a Product with a valid Amount Type") + + @api.model + def _get_general_account(self, user_id=None): + emp = self.env['hr.employee'].search([('user_id', '=', user_id or self.env.uid)], limit=1) + if emp and emp.product_id.categ_id.property_account_expense_categ.id: + return emp.product_id.categ_id.property_account_expense_categ.id + else: + raise exceptions.ValidationError("This user or employee is not associated to a Product with a valid Financial Account") + + @api.model + def _get_analytic_journal(self, user_id=None): + emp = self.env['hr.employee'].search([('user_id', '=', user_id or self.env.uid)], limit=1) + if emp and emp.journal_id.id: + return emp.journal_id.id + else: + raise exceptions.ValidationError("This user or employee is not associated to a valid Journal ID") + + @api.one + @api.constrains('user_id') + def check_user_id(self): + if self.is_timesheet: + emp = self.env['hr.employee'].search([('user_id', '=', self.user_id.id)], limit=1) + if not emp: + raise exceptions.ValidationError("There is no employee defined for user " + self.user_id.name) + else: + if not emp.journal_id.id: + raise exceptions.ValidationError("The employee " + emp.name + " is not associated to a valid Analytic Journal. Please define one for him or select another user.") + elif not emp.product_id: + raise exceptions.ValidationError("The employee " + emp.name + " is not associated to a valid Product. Please define one for him or select another user.") + elif not emp.product_id.uom_id.id: + raise exceptions.ValidationError("The employee " + emp.name + " is not associated to a valid Product Unit of Measure. Please define one for him or select another user.") + elif not emp.product_id.categ_id.property_account_expense_categ.id: + raise exceptions.ValidationError("The employee " + emp.name + " is not associated to a valid Financial Account. Please define one for him or select another user.") + + @api.onchange('user_id') + def V8_on_change_user_id(self): + new_values = self.on_change_user_id(self.user_id.id, self.is_timesheet) + if new_values.get("value"): + for key, value in new_values["value"].iteritems(): + setattr(self, key, value) + + def on_change_user_id(self, cr, uid, ids, user_id, is_timesheet=False, context=None): + if not is_timesheet or not user_id: + return {} + res = {"value": { + 'journal_id': self._get_analytic_journal(cr, uid, user_id, context=context), + 'product_id': self._get_employee_product(cr, uid, user_id, context=context), + 'uom_id': self._get_employee_unit(cr, uid, user_id, context=context), + 'general_account_id': self._get_general_account(cr, uid, user_id, context=context) + } + } + return res def on_change_unit_amount(self, cr, uid, id, prod_id, unit_amount, company_id, unit=False, journal_id=False, context=None): - res = {'value':{}} + res = {'value': {}} if prod_id and unit_amount: # find company company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=context) - r = self.pool.get('account.analytic.line').on_change_unit_amount(cr, uid, id, prod_id, unit_amount, company_id, unit, journal_id, context=context) + r = super(account_analytic_line, self).on_change_unit_amount(cr, uid, id, prod_id, unit_amount, company_id, unit, journal_id, context=context) if r: res.update(r) # update unit of measurement @@ -103,129 +145,40 @@ class hr_analytic_timesheet(osv.osv): res['value'].update({'product_uom_id': False}) return res - def _getEmployeeProduct(self, cr, uid, context=None): - if context is None: - context = {} - emp_obj = self.pool.get('hr.employee') - emp_id = emp_obj.search(cr, uid, [('user_id', '=', context.get('user_id') or uid)], context=context) - if emp_id: - emp = emp_obj.browse(cr, uid, emp_id[0], context=context) - if emp.product_id: - return emp.product_id.id - return False - - def _getEmployeeUnit(self, cr, uid, context=None): - emp_obj = self.pool.get('hr.employee') - if context is None: - context = {} - emp_id = emp_obj.search(cr, uid, [('user_id', '=', context.get('user_id') or uid)], context=context) - if emp_id: - emp = emp_obj.browse(cr, uid, emp_id[0], context=context) - if emp.product_id: - return emp.product_id.uom_id.id - return False - - def _getGeneralAccount(self, cr, uid, context=None): - emp_obj = self.pool.get('hr.employee') - if context is None: - context = {} - emp_id = emp_obj.search(cr, uid, [('user_id', '=', context.get('user_id') or uid)], context=context) - if emp_id: - emp = emp_obj.browse(cr, uid, emp_id[0], context=context) - if bool(emp.product_id): - a = emp.product_id.property_account_expense.id - if not a: - a = emp.product_id.categ_id.property_account_expense_categ.id - if a: - return a - return False - - def _getAnalyticJournal(self, cr, uid, context=None): - emp_obj = self.pool.get('hr.employee') - if context is None: - context = {} - if context.get('employee_id'): - emp_id = [context.get('employee_id')] - else: - emp_id = emp_obj.search(cr, uid, [('user_id','=',context.get('user_id') or uid)], limit=1, context=context) - if not emp_id: - model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'hr', 'open_view_employee_list_my') - msg = _("Employee is not created for this user. Please create one from configuration panel.") - raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel')) - emp = emp_obj.browse(cr, uid, emp_id[0], context=context) - if emp.journal_id: - return emp.journal_id.id - else : - raise UserError(_('No analytic journal defined for \'%s\'.\nYou should assign an analytic journal on the employee form.') % (emp.name,)) - - - _defaults = { - 'product_uom_id': _getEmployeeUnit, - 'product_id': _getEmployeeProduct, - 'general_account_id': _getGeneralAccount, - 'journal_id': _getAnalyticJournal, - 'date': lambda self, cr, uid, ctx: ctx.get('date', fields.date.context_today(self,cr,uid,context=ctx)), - 'user_id': lambda obj, cr, uid, ctx: ctx.get('user_id') or uid, - } - def on_change_account_id(self, cr, uid, ids, account_id, context=None): - return {'value':{}} - - def on_change_date(self, cr, uid, ids, date): - if ids: - new_date = self.read(cr, uid, ids[0], ['date'])['date'] - if date != new_date: - warning = {'title':_('User Alert!'),'message':_('Changing the date will let this entry appear in the timesheet of the new date.')} - return {'value':{},'warning':warning} - return {'value':{}} - - def create(self, cr, uid, vals, context=None): - if context is None: - context = {} - emp_obj = self.pool.get('hr.employee') - emp_id = emp_obj.search(cr, uid, [('user_id', '=', context.get('user_id') or uid)], context=context) - ename = '' - if emp_id: - ename = emp_obj.browse(cr, uid, emp_id[0], context=context).name - if not vals.get('journal_id'): - raise UserError(_('No \'Analytic Journal\' is defined for employee %s \nDefine an employee for the selected user and assign an \'Analytic Journal\'!') % (ename,)) - if not vals.get('account_id'): - raise UserError(_('No analytic account is defined on the project.\nPlease set one or we cannot automatically fill the timesheet.')) - return super(hr_analytic_timesheet, self).create(cr, uid, vals, context=context) - - def on_change_user_id(self, cr, uid, ids, user_id): - if not user_id: - return {} - context = {'user_id': user_id} - return {'value': { - 'product_id': self. _getEmployeeProduct(cr, uid, context), - 'product_uom_id': self._getEmployeeUnit(cr, uid, context), - 'general_account_id': self._getGeneralAccount(cr, uid, context), - 'journal_id': self._getAnalyticJournal(cr, uid, context), - }} + @api.onchange('date') + def on_change_date(self): + if self.is_timesheet and self._origin.date and (self._origin.date != self.date): + raise exceptions.Warning(_('Changing the date will let this entry appear in the timesheet of the new date.')) -class account_analytic_account(osv.osv): +class account_analytic_account(models.Model): _inherit = 'account.analytic.account' - _description = 'Analytic Account' - _columns = { - 'use_timesheets': fields.boolean('Timesheets', help="Check this field if this project manages timesheets", deprecated=True), - 'invoice_on_timesheets': fields.boolean('Timesheets', help="Check this field if this project manages timesheets"), - } - def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None): - res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context) - if template_id and 'value' in res: - template = self.browse(cr, uid, template_id, context=context) - res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets - return res + use_timesheets = fields.Boolean('Timesheets', help="Check this field if this project manages timesheets", deprecated=True) + invoice_on_timesheets = fields.Boolean('Timesheets', help="Check this field if this project manages timesheets") - def onchange_invoice_on_timesheets(self, cr, uid, ids, invoice_on_timesheets, context=None): + @api.onchange('invoice_on_timesheets') + def onchange_invoice_on_timesheets(self): result = {} - if not invoice_on_timesheets: + if not self.invoice_on_timesheets: return {'value': {'to_invoice': False}} try: - to_invoice = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'hr_timesheet_invoice.timesheet_invoice_factor1') + to_invoice = self.env['ir.model.data'].xmlid_to_res_id('hr_timesheet_invoice.timesheet_invoice_factor1') result['to_invoice'] = to_invoice except ValueError: pass return result + + @api.onchange('template_id') + def V8_on_change_template(self): + new_values = self.on_change_template(selftemplate_id.id, self.date_start) + if new_values.get("value"): + for key, value in new_values["value"].iteritems(): + setattr(self, key, value) + + def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None): + res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context) + if template_id and 'value' in res: + template = self.browse(cr, uid, template_id, context=context) + res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets + return res diff --git a/addons/hr_timesheet/hr_timesheet_demo.xml b/addons/hr_timesheet/hr_timesheet_demo.xml index 312f118a619..85df3005d15 100644 --- a/addons/hr_timesheet/hr_timesheet_demo.xml +++ b/addons/hr_timesheet/hr_timesheet_demo.xml @@ -26,7 +26,7 @@ <field name="invoice_on_timesheets">True</field> </record> - <record id="working_hours_requirements" model="hr.analytic.timesheet"> + <record id="working_hours_requirements" model="account.analytic.line"> <field name="name">Requirements analysis and specification</field> <field name="user_id" ref="base.user_root"/> <field eval="time.strftime('%Y-%m-%d')" name="date"/> @@ -37,9 +37,10 @@ <field eval="-60.00" name="amount"/> <field name="general_account_id" ref="account.a_expense"/> <field name="journal_id" ref="analytic_journal"/> + <field name="is_timesheet" eval="True"/> </record> - <record id="working_hours_design" model="hr.analytic.timesheet"> + <record id="working_hours_design" model="account.analytic.line"> <field name="name">Design and specification</field> <field name="user_id" ref="base.user_root"/> <field eval="time.strftime('%Y-%m-%d')" name="date"/> @@ -50,9 +51,10 @@ <field eval="-30.00" name="amount"/> <field name="general_account_id" ref="account.a_expense"/> <field name="journal_id" ref="analytic_journal"/> + <field name="is_timesheet" eval="True"/> </record> - <record id="working_hours_coding" model="hr.analytic.timesheet"> + <record id="working_hours_coding" model="account.analytic.line"> <field name="name">Coding and module testing</field> <field name="user_id" ref="base.user_root"/> <field eval="time.strftime('%Y-%m-%d')" name="date"/> @@ -63,9 +65,10 @@ <field eval="-90.00" name="amount"/> <field name="general_account_id" ref="account.a_expense"/> <field name="journal_id" ref="analytic_journal"/> + <field name="is_timesheet" eval="True"/> </record> - <record id="working_hours_testing" model="hr.analytic.timesheet"> + <record id="working_hours_testing" model="account.analytic.line"> <field name="name">Integration and system testing</field> <field name="user_id" ref="base.user_root"/> <field eval="time.strftime('%Y-%m-%d')" name="date"/> @@ -76,9 +79,10 @@ <field eval="-30.00" name="amount"/> <field name="general_account_id" ref="account.a_expense"/> <field name="journal_id" ref="analytic_journal"/> + <field name="is_timesheet" eval="True"/> </record> - <record id="working_hours_maintenance" model="hr.analytic.timesheet"> + <record id="working_hours_maintenance" model="account.analytic.line"> <field name="name">Delivery and maintenance</field> <field name="user_id" ref="base.user_root"/> <field eval="time.strftime('%Y-%m-%d')" name="date"/> @@ -89,6 +93,7 @@ <field eval="-30.00" name="amount"/> <field name="general_account_id" ref="account.a_expense"/> <field name="journal_id" ref="analytic_journal"/> + <field name="is_timesheet" eval="True"/> </record> <record id="product.product_product_consultant" model="product.product"> diff --git a/addons/hr_timesheet/hr_timesheet_view.xml b/addons/hr_timesheet/hr_timesheet_view.xml index 3ec03627932..ec83e80b2ca 100644 --- a/addons/hr_timesheet/hr_timesheet_view.xml +++ b/addons/hr_timesheet/hr_timesheet_view.xml @@ -3,12 +3,12 @@ <data> <record id="hr_timesheet_line_tree" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.tree</field> - <field name="model">hr.analytic.timesheet</field> + <field name="name">account.analytic.line.tree</field> + <field name="model">account.analytic.line</field> <field name="arch" type="xml"> <tree editable="top" string="Timesheet Activities"> - <field name="date" on_change="on_change_date(date)"/> - <field name="user_id" on_change="on_change_user_id(user_id)" required="1" options='{"no_open": True}' + <field name="date"/> + <field name="user_id" required="1" options='{"no_open": True}' context="{'default_groups_ref': ['base.group_user']}"/> <field name="name"/> <field domain="[('type','=','normal'),('invoice_on_timesheets','=',1)]" name="account_id" context="{'default_use_timesheets': 1, 'default_type': 'contract'}" options="{'no_open': True, 'no_create': True}"/> @@ -18,23 +18,25 @@ <field name="amount" sum="Total cost" invisible="1"/> <field name="general_account_id" invisible="1"/> <field name="product_id" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" required="1" domain="[('type','=','service')]" invisible="1"/> + <field name="is_timesheet" invisible="1"/> </tree> </field> </record> <record id="hr_timesheet_line_form" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.form</field> - <field name="model">hr.analytic.timesheet</field> + <field name="name">account.analytic.line.form</field> + <field name="model">account.analytic.line</field> <field name="arch" type="xml"> <form string="Timesheet Activities"> <sheet> <group> <group> <field name="name"/> - <field name="user_id" on_change="on_change_user_id(user_id)" required="1" + <field name="user_id" required="1" context="{'default_groups_ref': ['base.group_user']}"/> + <field name="is_timesheet" invisible="1"/> </group> <group> - <field name="date" on_change="on_change_date(date)"/> + <field name="date"/> </group> </group> <notebook> @@ -44,8 +46,8 @@ <field name="product_id" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" required="1" domain="[('type','=','service')]"/> <label string="Duration" for="unit_amount"/> <div> - <field name="unit_amount" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" class="oe_inline"/> - <field name="product_uom_id" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" class="oe_inline"/> + <field name="unit_amount" class="oe_inline" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)"/> + <field name="product_uom_id" class="oe_inline"/> </div> </group> <group string="Accounting"> @@ -61,10 +63,9 @@ </form> </field> </record> - <record id="hr_timesheet_line_search" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.search</field> - <field name="model">hr.analytic.timesheet</field> + <field name="name">account.analytic.line.search</field> + <field name="model">account.analytic.line</field> <field name="arch" type="xml"> <search string="Timesheet"> <field name="date"/> @@ -87,7 +88,7 @@ <field name="res_model">account.analytic.line</field> <field name="src_model">account.analytic.account</field> <field name="view_mode">tree,form</field> - <field name="view_type">form</field> + <field name="view_type">tree</field> <field name="help" type="html"> <p> No activity yet on this contract. @@ -114,7 +115,7 @@ <field name="inherit_id" ref="analytic.view_account_analytic_account_form"/> <field name="arch" type="xml"> <xpath expr='//div[@name="project"]' position='inside'> - <field name="invoice_on_timesheets" on_change="onchange_invoice_on_timesheets(invoice_on_timesheets)"/> + <field name="invoice_on_timesheets"/> <label for="invoice_on_timesheets"/> </xpath> <xpath expr='//div[@name="buttons"]' position='inside'> @@ -140,11 +141,10 @@ <record id="act_hr_timesheet_line_evry1_all_form" model="ir.actions.act_window"> <field name="name">Activities</field> - <field name="type">ir.actions.act_window</field> - <field name="res_model">hr.analytic.timesheet</field> + <field name="res_model">account.analytic.line</field> <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="context">{"search_default_today":1}</field> + <field name="view_id" ref="hr_timesheet_line_form"/> + <field name="context">{"search_default_today":1, "default_is_timesheet":1}</field> <field name="search_view_id" ref="hr_timesheet_line_search"/> <field name="help" type="html"> <p class="oe_view_nocontent_create"> @@ -158,8 +158,14 @@ </field> </record> - <menuitem id="menu_hr_working_hours" parent="hr_attendance.menu_hr_time_tracking" action="act_hr_timesheet_line_evry1_all_form" - groups="base.group_no_one"/> + <record model="ir.actions.act_window.view" id="action_open_timesheet_tree"> + <field name="sequence" eval="1"/> + <field name="view_mode">tree</field> + <field name="view_id" ref="hr_timesheet_line_tree"/> + <field name="act_window_id" ref="act_hr_timesheet_line_evry1_all_form"/> + </record> + + <menuitem id="menu_hr_working_hours" parent="hr_attendance.menu_hr_time_tracking" action="act_hr_timesheet_line_evry1_all_form"/> <record id="hr_timesheet_employee_extd_form" model="ir.ui.view"> <field name="name">hr.timesheet.employee.extd_form</field> diff --git a/addons/hr_timesheet/report/hr_timesheet_report.py b/addons/hr_timesheet/report/hr_timesheet_report.py index 20673770c40..1566f83fd79 100644 --- a/addons/hr_timesheet/report/hr_timesheet_report.py +++ b/addons/hr_timesheet/report/hr_timesheet_report.py @@ -1,28 +1,27 @@ -from openerp import tools -from openerp.osv import fields,osv +from openerp import tools, models, fields from openerp.addons.decimal_precision import decimal_precision as dp -class hr_timesheet_report(osv.osv): + +class hr_timesheet_report(models.Model): _name = "hr.timesheet.report" _description = "Timesheet" _auto = False - _columns = { - 'date': fields.date('Date', readonly=True), - 'name': fields.char('Description', size=64,readonly=True), - 'product_id' : fields.many2one('product.product', 'Product',readonly=True), - 'journal_id' : fields.many2one('account.analytic.journal', 'Journal',readonly=True), - 'general_account_id' : fields.many2one('account.account', 'General Account', readonly=True), - 'user_id': fields.many2one('res.users', 'User',readonly=True), - 'account_id': fields.many2one('account.analytic.account', 'Analytic Account',readonly=True), - 'company_id': fields.many2one('res.company', 'Company',readonly=True), - 'cost': fields.float('Cost', readonly=True, digits_compute=dp.get_precision('Account')), - 'quantity': fields.float('Time', readonly=True), # TDE FIXME master: rename into time - } + + date = fields.Date('Date', readonly=True) + name = fields.Char('Description', size=64, readonly=True) + product_id = fields.Many2one('product.product', 'Product', readonly=True) + journal_id = fields.Many2one('account.analytic.journal', 'Journal', readonly=True) + general_account_id = fields.Many2one('account.account', 'General Account', readonly=True) + user_id = fields.Many2one('res.users', 'User', readonly=True) + account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True) + company_id = fields.Many2one('res.company', 'Company', readonly=True) + cost = fields.Float('Cost', readonly=True, digits_compute=dp.get_precision('Account')) + quantity = fields.Float('Time', readonly=True) # TDE FIXME master: rename into time def _select(self): select_str = """ - SELECT min(hat.id) as id, + SELECT min(aal.id) as id, aal.date as date, sum(aal.amount) as cost, sum(aal.unit_amount) as quantity, @@ -38,8 +37,7 @@ class hr_timesheet_report(osv.osv): def _from(self): from_str = """ - account_analytic_line as aal - inner join hr_analytic_timesheet as hat ON (hat.line_id=aal.id) + FROM account_analytic_line as aal """ return from_str @@ -56,11 +54,18 @@ class hr_timesheet_report(osv.osv): """ return group_by_str + def _where(self): + where_str = """ + WHERE aal.is_timesheet IS TRUE + """ + return where_str + def init(self, cr): # self._table = hr_timesheet_report tools.drop_view_if_exists(cr, self._table) cr.execute("""CREATE or REPLACE VIEW %s as ( %s - FROM ( %s ) %s - )""" % (self._table, self._select(), self._from(), self._group_by())) + %s + %s + )""" % (self._table, self._select(), self._from(), self._where(), self._group_by())) diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml deleted file mode 100644 index c5549d1ed27..00000000000 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<openerp> - <data noupdate="1"> - - <record id="property_rule_timesheet_manager" model="ir.rule"> - <field name="name">Manager HR Analytic Timesheet</field> - <field name="model_id" ref="model_hr_analytic_timesheet"/> - <field name="domain_force">[(1,'=',1)]</field> - <field name="groups" eval="[(4,ref('base.group_hr_manager')), (4,ref('base.group_hr_user'))]"/> - </record> - - <record id="property_rule_timesheet_employee" model="ir.rule"> - <field name="name">Employee HR Analytic Timesheet</field> - <field name="model_id" ref="model_hr_analytic_timesheet"/> - <field name="domain_force">[('user_id', '=', user.id)]</field> - <field name="groups" eval="[(4,ref('base.group_user'))]"/> - </record> - - </data> -</openerp> diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv index 757cd84f4b5..c6317873ac8 100644 --- a/addons/hr_timesheet/security/ir.model.access.csv +++ b/addons/hr_timesheet/security/ir.model.access.csv @@ -1,6 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_hr_analytic_timesheet,hr.analytic.timesheet,model_hr_analytic_timesheet,base.group_hr_user,1,1,1,1
-access_hr_analytic_timesheet_employee,hr.analytic.timesheet,model_hr_analytic_timesheet,base.group_user,1,1,1,1
access_hr_account_analytic_line,account.account.analytic.line,account.model_account_analytic_line,base.group_hr_user,1,1,1,1
access_hr_account_analytic_line_employee,account.account.analytic.line employee,account.model_account_analytic_line,base.group_user,1,1,1,1
access_account_analytic_journal,account.account.analytic.journal,analytic.model_account_analytic_journal,base.group_hr_user,1,1,1,1
@@ -8,6 +6,6 @@ access_product_product_user,product.product user,product.model_product_product,b access_product_template_hr_timesheet,product.template.hr.timesheet,product.model_product_template,base.group_hr_user,1,1,1,1
access_product_uom_hr_timesheet,product.uom.hr.timesheet,product.model_product_uom,base.group_hr_user,1,1,1,1
access_account_fiscalyear_hr_user,account.account.fiscalyear.user,account.model_account_fiscalyear,base.group_hr_user,1,1,1,1
-access_hr_analytic_timesheet_user,hr.analytic.timesheet.user,model_hr_analytic_timesheet,base.group_user,1,1,1,0
+access_account_analytic_line_user,account.analytic.line.user,model_account_analytic_line,base.group_user,1,1,1,0
access_account_analytic_journal_user,account.account.analytic.journal.user,analytic.model_account_analytic_journal,base.group_user,1,0,0,0
-access_hr_timesheet_report,hr.timesheet.report,model_hr_timesheet_report,base.group_hr_manager,1,1,0,0
+access_hr_timesheet_report,hr.timesheet.report,model_hr_timesheet_report,base.group_hr_manager,1,1,0,0
\ No newline at end of file diff --git a/addons/hr_timesheet/test/hr_timesheet_demo.yml b/addons/hr_timesheet/test/hr_timesheet_demo.yml deleted file mode 100644 index 2c81912d130..00000000000 --- a/addons/hr_timesheet/test/hr_timesheet_demo.yml +++ /dev/null @@ -1,11 +0,0 @@ -- - Give the access rights of Hr Officer to create employee. -- - !context - uid: 'res_hr_timesheet_officer' -- - !record {model: hr.analytic.timesheet, id: working_hours_coding, view: False}: - user_id: base.user_demo - date: !eval time.strftime('%Y-%m-%d') - account_id: analytic.analytic_administratif - unit_amount: 1.0 diff --git a/addons/hr_timesheet_invoice/__openerp__.py b/addons/hr_timesheet_invoice/__openerp__.py index 1eb003b1e4f..35630b0b3df 100644 --- a/addons/hr_timesheet_invoice/__openerp__.py +++ b/addons/hr_timesheet_invoice/__openerp__.py @@ -34,7 +34,8 @@ You can define price lists in analytic account, make some theoretical revenue reports.""", 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'depends': ['account', 'hr_timesheet', 'report'], + 'images': ['images/hr_bill_task_work.jpeg','images/hr_type_of_invoicing.jpeg'], + 'depends': ['hr_timesheet', 'report'], 'data': [ 'security/ir.model.access.csv', 'hr_timesheet_invoice_data.xml', diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index 05e772e30e9..3e26dec67a9 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -39,38 +39,13 @@ class hr_timesheet_invoice_factor(osv.osv): } - class account_analytic_account(osv.osv): - def _invoiced_calc(self, cr, uid, ids, name, arg, context=None): - obj_invoice = self.pool.get('account.invoice') - res = {} - - cr.execute('SELECT account_id as account_id, l.invoice_id ' - 'FROM hr_analytic_timesheet h LEFT JOIN account_analytic_line l ' - 'ON (h.line_id=l.id) ' - 'WHERE l.account_id = ANY(%s)', (ids,)) - account_to_invoice_map = {} - for rec in cr.dictfetchall(): - account_to_invoice_map.setdefault(rec['account_id'], []).append(rec['invoice_id']) - - for account in self.browse(cr, uid, ids, context=context): - invoice_ids = filter(None, list(set(account_to_invoice_map.get(account.id, [])))) - for invoice in obj_invoice.browse(cr, uid, invoice_ids, context=context): - res.setdefault(account.id, 0.0) - res[account.id] += invoice.amount_untaxed - for id in ids: - res[id] = round(res.get(id, 0.0),2) - - return res - _inherit = "account.analytic.account" _columns = { 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help="The product to invoice is defined on the employee form, the price will be deducted by this pricelist on the product."), 'amount_max': fields.float('Max. Invoice Price', help="Keep empty if this contract is not limited to a total fixed price."), - 'amount_invoiced': fields.function(_invoiced_calc, string='Invoiced Amount', - help="Total invoiced"), 'to_invoice': fields.many2one('hr_timesheet_invoice.factor', 'Timesheet Invoicing Ratio', help="You usually invoice 100% of the timesheets. But if you mix fixed price and timesheet invoicing, you may use another ratio. For instance, if you do a 20% advance invoice (fixed price, based on a sales order), you should invoice the rest on timesheet with a 80% ratio."), } @@ -78,7 +53,7 @@ class account_analytic_account(osv.osv): _defaults = { 'pricelist_id': lambda self, cr, uid, c: self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'product.list0') } - + def on_change_partner_id(self, cr, uid, ids, partner_id, name, context=None): res = super(account_analytic_account, self).on_change_partner_id(cr, uid, ids, partner_id, name, context=context) if partner_id: @@ -108,14 +83,6 @@ class account_analytic_line(osv.osv): 'to_invoice': fields.many2one('hr_timesheet_invoice.factor', 'Invoiceable', help="It allows to set the discount while making invoice, keep empty if the activities should not be invoiced."), } - def _default_journal(self, cr, uid, context=None): - proxy = self.pool.get('hr.employee') - record_ids = proxy.search(cr, uid, [('user_id', '=', uid)], context=context) - if record_ids: - employee = proxy.browse(cr, uid, record_ids[0], context=context) - return employee.journal_id and employee.journal_id.id or False - return False - def _default_general_account(self, cr, uid, context=None): proxy = self.pool.get('hr.employee') record_ids = proxy.search(cr, uid, [('user_id', '=', uid)], context=context) @@ -126,30 +93,34 @@ class account_analytic_line(osv.osv): return False _defaults = { - 'journal_id' : _default_journal, - 'general_account_id' : _default_general_account, + 'general_account_id': _default_general_account, } def write(self, cr, uid, ids, vals, context=None): self._check_inv(cr, uid, ids, vals) - return super(account_analytic_line,self).write(cr, uid, ids, vals, + return super(account_analytic_line, self).write(cr, uid, ids, vals, context=context) - def _check_inv(self, cr, uid, ids, vals): + def unlink(self, cr, uid, ids, context=None): + if any(line.invoice_id.id for line in self.browse(cr, uid, ids, context)): + raise UserError(_('You cannot delete an invoiced analytic line!')) + return super(account_analytic_line, self).unlink(cr, uid, ids, context) + + def _check_inv(self, cr, uid, ids, vals, context=None): select = ids if isinstance(select, (int, long)): select = [ids] - if ( not vals.has_key('invoice_id')) or vals['invoice_id' ] == False: - for line in self.browse(cr, uid, select): + if (not vals.has_key('invoice_id')) or vals['invoice_id'] == False: + for line in self.browse(cr, uid, select, context): if line.invoice_id: raise UserError(_('You cannot modify an invoiced analytic line!')) return True - def _get_invoice_price(self, cr, uid, account, product_id, user_id, qty, context = {}): + def _get_invoice_price(self, cr, uid, account, product_id, user_id, qty, context={}): pro_price_obj = self.pool.get('product.pricelist') if account.pricelist_id: pl = account.pricelist_id.id - price = pro_price_obj.price_get(cr,uid,[pl], product_id, qty or 1.0, account.partner_id.id, context=context)[pl] + price = pro_price_obj.price_get(cr, uid, [pl], product_id, qty or 1.0, account.partner_id.id, context=context)[pl] else: price = 0.0 return price @@ -337,24 +308,22 @@ class account_analytic_line(osv.osv): invoice_obj.button_reset_taxes(cr, uid, [last_invoice], context) return invoices - -class hr_analytic_timesheet(osv.osv): - _inherit = "hr.analytic.timesheet" - def on_change_account_id(self, cr, uid, ids, account_id, user_id=False): - res = {} - if not account_id: - return res - res.setdefault('value',{}) - acc = self.pool.get('account.analytic.account').browse(cr, uid, account_id) - st = acc.to_invoice.id - res['value']['to_invoice'] = st or False - if acc.state=='pending': - res['warning'] = { - 'title': 'Warning', - 'message': 'The analytic account is in pending state.\nYou should not work on this account !' - } + def on_change_account_id(self, cr, uid, ids, account_id, user_id=False, is_timesheet=False, context=None): + res = {'value': {}} + if is_timesheet and account_id: + acc = self.pool.get('account.analytic.account').browse(cr, uid, account_id, context=context) + st = acc.to_invoice.id + res['value']['to_invoice'] = st or False + if acc.state == 'pending': + res['warning'] = { + 'title': 'Warning', + 'message': 'The analytic account is in pending state.\nYou should not work on this account !' + } + elif acc.state == 'close' or acc.state == 'cancelled': + raise osv.except_osv(_('Invalid Analytic Account!'), _('You cannot select a Analytic Account which is in Close or Cancelled state.')) return res + class account_invoice(osv.osv): _inherit = "account.invoice" @@ -366,23 +335,22 @@ class account_invoice(osv.osv): obj_analytic_account = self.pool.get('account.analytic.account') for il in iml: if il['account_analytic_id']: - # *-* browse (or refactor to avoid read inside the loop) + # *-* browse (or refactor to avoid read inside the loop) to_invoice = obj_analytic_account.read(cr, uid, [il['account_analytic_id']], ['to_invoice'], context=context)[0]['to_invoice'] if to_invoice: il['analytic_lines'][0][2]['to_invoice'] = to_invoice[0] return iml - class account_move_line(osv.osv): _inherit = "account.move.line" def create_analytic_lines(self, cr, uid, ids, context=None): - res = super(account_move_line, self).create_analytic_lines(cr, uid, ids,context=context) + res = super(account_move_line, self).create_analytic_lines(cr, uid, ids, context=context) analytic_line_obj = self.pool.get('account.analytic.line') for move_line in self.browse(cr, uid, ids, context=context): - #For customer invoice, link analytic line to the invoice so it is not proposed for invoicing in Bill Tasks Work - invoice_id = move_line.invoice and move_line.invoice.type in ('out_invoice','out_refund') and move_line.invoice.id or False + # For customer invoice, link analytic line to the invoice so it is not proposed for invoicing in Bill Tasks Work + invoice_id = move_line.invoice and move_line.invoice.type in ('out_invoice', 'out_refund') and move_line.invoice.id or False for line in move_line.analytic_lines: analytic_line_obj.write(cr, uid, line.id, { 'invoice_id': invoice_id, diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml index 55aaa0d50ae..967da3c3f17 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml @@ -12,7 +12,7 @@ <field name="pricelist_id" groups="product.group_sale_pricelist"/> <field name="to_invoice"/> </group> - </xpath> + </xpath> <xpath expr="/form/sheet" position='before'> <header> <button name="set_pending" string="To Renew" type="object" states="open" /> @@ -21,111 +21,59 @@ <button name="set_cancel" string="Cancel Contract" type="object" states="open,pending"/> <field name="state" readonly="1" widget="statusbar" statusbar_visible="open,pending,close" statusbar_colors='{"pending":"red", "template":"blue"}'/> - </header> - </xpath> - </field> - </record> - - <record id="hr_timesheet_line_form" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.form</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/> - <field name="arch" type="xml"> - <xpath expr="//group[@string='Accounting']" position='after'> - <group string="Invoicing"> - <field name="to_invoice"/> - <field name="invoice_id"/> - </group> - </xpath> - </field> - </record> - - <record id="hr_timesheet_line_form2" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.form2</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/> - <field name="arch" type="xml"> - <field name="account_id" position="attributes"> - <attribute name="domain">[('type','in',['normal','contract']),('state', '!=', 'close'),('invoice_on_timesheets','=',1)]</attribute> - <attribute name="on_change">on_change_account_id(account_id, user_id)</attribute> - <attribute name="context">{'default_invoice_on_timesheets': 1}</attribute> + </header> + </xpath> </field> - </field> - </record> + </record> - <record id="hr_timesheet_line_tree" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.tree</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/> - <field name="arch" type="xml"> - <field name="amount" position="after"> - <field name="to_invoice"/> + <record id="hr_timesheet_line_form" model="ir.ui.view"> + <field name="name">account.analytic.line.form</field> + <field name="model">account.analytic.line</field> + <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/> + <field name="arch" type="xml"> + <xpath expr="//group[@string='Accounting']" position='after'> + <group string="Invoicing"> + <field name="to_invoice"/> + <field name="invoice_id" readonly="1"/> + </group> + </xpath> + <field name="account_id" position="attributes"> + <attribute name="domain">[('type','in',['normal','contract']),('state', '!=', 'close'),('invoice_on_timesheets','=',1)]</attribute> + <attribute name="on_change">on_change_account_id(account_id, user_id, is_timesheet, context)</attribute> + <attribute name="context">{'default_invoice_on_timesheets': 1}</attribute> + </field> </field> - </field> - </record> + </record> - <record id="hr_timesheet_line_tree2" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.tree2</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/> - <field name="arch" type="xml"> - <field name="account_id" position="attributes"> - <attribute name="domain">[('type','in',['normal','contract']),('state', '!=', 'close'),('invoice_on_timesheets','=',1)]</attribute> - <attribute name="on_change">on_change_account_id(account_id, user_id)</attribute> - <attribute name="context">{'default_invoice_on_timesheets': 1}</attribute> + <record id="hr_timesheet_line_tree" model="ir.ui.view"> + <field name="name">account.analytic.line.tree</field> + <field name="model">account.analytic.line</field> + <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/> + <field name="arch" type="xml"> + <field name="amount" position="after"> + <field name="to_invoice"/> + <field name="invoice_id" readonly="1"/> + </field> + <field name="account_id" position="attributes"> + <attribute name="domain">[('type','in',['normal','contract']),('state', '!=', 'close'),('invoice_on_timesheets','=',1)]</attribute> + <attribute name="on_change">on_change_account_id(account_id, user_id, is_timesheet, context)</attribute> + <attribute name="context">{'default_invoice_on_timesheets': 1}</attribute> + </field> </field> - </field> - </record> - - <!-- Inherits for account analytic lines --> - <record id="view_account_analytic_line_search_inherit" model="ir.ui.view"> - <field name="name">account.analytic.line.search.to_invoice</field> - <field name="model">account.analytic.line</field> - <field name="inherit_id" ref="analytic.view_account_analytic_line_filter"/> - <field name="arch" type="xml"> - <xpath expr="//field[@name='date']" position="after"> - <separator/> - <filter name="to_invoice" string="To Invoice" context="{'to_invoice': 1}" domain="[('invoice_id','=',False), ('to_invoice','<>',False)]" icon="terp-dolar"/> - <filter name="invoiced" string="Invoiced" domain="[('invoice_id','!=',False), ('to_invoice','<>',False)]" icon="terp-dolar"/> - </xpath> - </field> - </record> - - <record id="view_hr_timesheet_line_search_to_invoice" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.search.to_invoice</field> - <field name="model">hr.analytic.timesheet</field> - <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_search"/> - <field name="arch" type="xml"> - <xpath expr="//field[@name='account_id']" position="after"> - <separator/> - <filter name="to_invoice" string="To Invoice" context="{'to_invoice': 1}" domain="[('invoice_id','=',False), ('to_invoice','<>',False)]" icon="terp-dolar"/> - <filter name="invoiced" string="Invoiced" domain="[('invoice_id','!=',False), ('to_invoice','<>',False)]" icon="terp-dolar"/> - </xpath> - </field> - </record> + </record> - - <record id="view_account_analytic_line_tree_inherit" model="ir.ui.view"> - <field name="name">account.analytic.line.tree.to_invoice</field> - <field name="model">account.analytic.line</field> - <field name="inherit_id" ref="analytic.view_account_analytic_line_tree"/> - <field name="arch" type="xml"> - <field name="account_id" position="after"> - <field name="to_invoice"/> - </field> - </field> - </record> - <record id="view_account_analytic_line_form_inherit" model="ir.ui.view"> - <field name="name">account.analytic.line.form.to_invoice</field> - <field name="model">account.analytic.line</field> - <field name="inherit_id" ref="analytic.view_account_analytic_line_form"/> - <field name="arch" type="xml"> - <field name="amount" position="before"> - <field name="to_invoice"/> - <field name="invoice_id"/> + <record id="account_analytic_line_view_search_to_invoice" model="ir.ui.view"> + <field name="name">account.analytic.line.search.to_invoice</field> + <field name="model">account.analytic.line</field> + <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_search"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='account_id']" position="after"> + <separator/> + <filter name="to_invoice" string="To Invoice" context="{'to_invoice': 1}" domain="[('invoice_id','=',False), ('to_invoice','!=',False)]" icon="terp-dolar"/> + <filter name="invoiced" string="Invoiced" domain="[('invoice_id','!=',False), ('to_invoice','!=',False)]" icon="terp-dolar"/> + </xpath> </field> - </field> - </record> + </record> <record id="view_hr_timesheet_invoice_factor_search" model="ir.ui.view"> <field name="name">hr_timesheet_invoice.factor.search</field> @@ -163,6 +111,7 @@ <record id="action_hr_timesheet_invoice_factor_form" model="ir.actions.act_window"> <field name="name">Invoicing Types</field> <field name="type">ir.actions.act_window</field> + <field name="context">{"default_is_timesheet":1}</field> <field name="res_model">hr_timesheet_invoice.factor</field> <field name="view_type">form</field> <field name="view_id" ref="view_hr_timesheet_invoice_factor_tree"/> @@ -170,19 +119,19 @@ <field name="help" type="html"> <p class="oe_view_nocontent_create"> Click to add a new type of invoicing. - </p><p> - Odoo allows you to create default invoicing types. You might - have to regularly assign discounts because of a specific - contract or agreement with a customer. From this menu, you can - create additional types of invoicing to speed up your - invoicing. - </p> - </field> - </record> - <menuitem - action="action_hr_timesheet_invoice_factor_form" - id="hr_timesheet_invoice_factor_view" - parent="account.menu_analytic_accounting" sequence="99" groups="base.group_no_one"/> + </p><p> + Odoo allows you to create default invoicing types. You might + have to regularly assign discounts because of a specific + contract or agreement with a customer. From this menu, you can + create additional types of invoicing to speed up your + invoicing. + </p> + </field> +</record> +<menuitem + action="action_hr_timesheet_invoice_factor_form" + id="hr_timesheet_invoice_factor_view" + parent="account.menu_analytic_accounting" sequence="99" groups="base.group_no_one"/> <record id="hr_timesheet.act_hr_timesheet_accounts_form" model="ir.actions.act_window"> <field name="context">{'invoice_on_timesheets':1, 'default_type': 'contract'}</field> diff --git a/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice.yml b/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice.yml index 3d852a53540..29bb6363ebe 100644 --- a/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice.yml +++ b/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice.yml @@ -28,7 +28,7 @@ - I assign account on analytic account line. - - !python {model: hr.analytic.timesheet}: | + !python {model: account.analytic.line}: | self.on_change_account_id(cr, uid, [ref('account_analytic_line_developyamlforhrmodule0')], ref('analytic.analytic_agrolait')) - diff --git a/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice_no_prod_tax.yml b/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice_no_prod_tax.yml index 15b243d0ac8..6c2adc1862a 100644 --- a/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice_no_prod_tax.yml +++ b/addons/hr_timesheet_invoice/test/test_hr_timesheet_invoice_no_prod_tax.yml @@ -28,7 +28,7 @@ - I assign account on analytic account line. - - !python {model: hr.analytic.timesheet}: | + !python {model: account.analytic.line}: | self.on_change_account_id(cr, uid, [ref('account_analytic_line_developyamlforhrmodule1')], ref('analytic.analytic_agrolait')) - I create a Tax Code diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py index 953cb1942aa..90a9c8550bf 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py @@ -139,16 +139,12 @@ class hr_timesheet_sheet(osv.osv): if sheet.employee_id.id not in employee_ids: employee_ids.append(sheet.employee_id.id) return hr_employee.attendance_action_change(cr, uid, employee_ids, context=context) - def _count_all(self, cr, uid, ids, field_name, arg, context=None): - Timesheet = self.pool['hr.analytic.timesheet'] - Attendance = self.pool['hr.attendance'] - return { - sheet_id: { - 'timesheet_activity_count': Timesheet.search_count(cr,uid, [('sheet_id','=', sheet_id)], context=context), - 'attendance_count': Attendance.search_count(cr,uid, [('sheet_id', '=', sheet_id)], context=context) - } - for sheet_id in ids - } + def _count_attendances(self, cr, uid, ids, field_name, arg, context=None): + res = dict.fromkeys(ids, 0) + attendances_groups = self.pool['hr.attendance'].read_group(cr, uid, [('sheet_id' , 'in' , ids)], ['sheet_id'], 'sheet_id', context=context) + for attendances in attendances_groups: + res[attendances['sheet_id'][0]] = attendances['sheet_id_count'] + return res _columns = { 'name': fields.char('Note', select=1, @@ -157,7 +153,7 @@ class hr_timesheet_sheet(osv.osv): 'user_id': fields.related('employee_id', 'user_id', type="many2one", relation="res.users", store=True, string="User", required=False, readonly=True),#fields.many2one('res.users', 'User', required=True, select=1, states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}), 'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}), 'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}), - 'timesheet_ids' : fields.one2many('hr.analytic.timesheet', 'sheet_id', + 'timesheet_ids' : fields.one2many('account.analytic.line', 'sheet_id', 'Timesheet lines', readonly=True, states={ 'draft': [('readonly', False)], @@ -181,8 +177,7 @@ class hr_timesheet_sheet(osv.osv): 'account_ids': fields.one2many('hr_timesheet_sheet.sheet.account', 'sheet_id', 'Analytic accounts', readonly=True), 'company_id': fields.many2one('res.company', 'Company'), 'department_id':fields.many2one('hr.department','Department'), - 'timesheet_activity_count': fields.function(_count_all, type='integer', string='Timesheet Activities', multi=True), - 'attendance_count': fields.function(_count_all, type='integer', string="Attendances", multi=True), + 'attendance_count': fields.function(_count_attendances, type='integer', string="Attendances"), } def _default_date_from(self, cr, uid, context=None): @@ -260,7 +255,7 @@ class hr_timesheet_sheet(osv.osv): raise UserError(_('You cannot delete a timesheet which have attendance entries.')) toremove = [] - analytic_timesheet = self.pool.get('hr.analytic.timesheet') + analytic_timesheet = self.pool.get('account.analytic.line') for sheet in self.browse(cr, uid, ids, context=context): for timesheet in sheet.timesheet_ids: toremove.append(timesheet.id) @@ -298,38 +293,36 @@ class hr_timesheet_sheet(osv.osv): return dom +class account_analytic_account(osv.osv): + _inherit = "account.analytic.account" + + def name_create(self, cr, uid, name, context=None): + if context is None: + context = {} + group_template_required = self.pool['res.users'].has_group(cr, uid, 'account_analytic_analysis.group_template_required') + if not context.get('default_invoice_on_timesheets') or group_template_required: + return super(account_analytic_account, self).name_create(cr, uid, name, context=context) + rec_id = self.create(cr, uid, {self._rec_name: name}, context) + return self.name_get(cr, uid, [rec_id], context)[0] + + class account_analytic_line(osv.osv): _inherit = "account.analytic.line" def _get_default_date(self, cr, uid, context=None): if context is None: context = {} - #get the default date (should be: today) + # get the default date (should be: today) res = super(account_analytic_line, self)._get_default_date(cr, uid, context=context) - #if we got the dates from and to from the timesheet and if the default date is in between, we use the default - #but if the default isn't included in those dates, we use the date start of the timesheet as default + # if we got the dates from and to from the timesheet and if the default date is in between, we use the default + # but if the default isn't included in those dates, we use the date start of the timesheet as default if context.get('timesheet_date_from') and context.get('timesheet_date_to'): if context['timesheet_date_from'] <= res <= context['timesheet_date_to']: return res return context.get('timesheet_date_from') - #if we don't get the dates from the timesheet, we return the default value from super() + # if we don't get the dates from the timesheet, we return the default value from super() return res -class account_analytic_account(osv.osv): - _inherit = "account.analytic.account" - - def name_create(self, cr, uid, name, context=None): - if context is None: - context = {} - group_template_required = self.pool['res.users'].has_group(cr, uid, 'account_analytic_analysis.group_template_required') - if not context.get('default_invoice_on_timesheets') or group_template_required: - return super(account_analytic_account, self).name_create(cr, uid, name, context=context) - rec_id = self.create(cr, uid, {self._rec_name: name}, context) - return self.name_get(cr, uid, [rec_id], context)[0] - -class hr_timesheet_line(osv.osv): - _inherit = "hr.analytic.timesheet" - def _sheet(self, cursor, user, ids, name, args, context=None): sheet_obj = self.pool.get('hr_timesheet_sheet.sheet') res = {}.fromkeys(ids, False) @@ -349,29 +342,22 @@ class hr_timesheet_line(osv.osv): for ts in self.browse(cr, uid, ids, context=context): cr.execute(""" SELECT l.id - FROM hr_analytic_timesheet l - INNER JOIN account_analytic_line al - ON (l.line_id = al.id) - WHERE %(date_to)s >= al.date - AND %(date_from)s <= al.date - AND %(user_id)s = al.user_id + FROM account_analytic_line l + WHERE %(date_to)s >= l.date + AND %(date_from)s <= l.date + AND %(user_id)s = l.user_id GROUP BY l.id""", {'date_from': ts.date_from, 'date_to': ts.date_to, 'user_id': ts.employee_id.user_id.id,}) ts_line_ids.extend([row[0] for row in cr.fetchall()]) return ts_line_ids - def _get_account_analytic_line(self, cr, uid, ids, context=None): - ts_line_ids = self.pool.get('hr.analytic.timesheet').search(cr, uid, [('line_id', 'in', ids)]) - return ts_line_ids - _columns = { 'sheet_id': fields.function(_sheet, string='Sheet', select="1", type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade", store={ 'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10), - 'account.analytic.line': (_get_account_analytic_line, ['user_id', 'date'], 10), - 'hr.analytic.timesheet': (lambda self,cr,uid,ids,context=None: ids, None, 10), + 'account.analytic.line': (lambda self,cr,uid,ids,context=None: ids, ['user_id', 'date'], 10), }, ), } @@ -380,13 +366,13 @@ class hr_timesheet_line(osv.osv): if isinstance(ids, (int, long)): ids = [ids] self._check(cr, uid, ids) - return super(hr_timesheet_line, self).write(cr, uid, ids, values, context=context) + return super(account_analytic_line, self).write(cr, uid, ids, values,context=context) def unlink(self, cr, uid, ids, *args, **kwargs): if isinstance(ids, (int, long)): ids = [ids] self._check(cr, uid, ids) - return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs) + return super(account_analytic_line,self).unlink(cr, uid, ids,*args, **kwargs) def _check(self, cr, uid, ids): for att in self.browse(cr, uid, ids): @@ -395,8 +381,7 @@ class hr_timesheet_line(osv.osv): return True def multi_on_change_account_id(self, cr, uid, ids, account_ids, context=None): - return dict([(el, self.on_change_account_id(cr, uid, ids, el, context.get('user_id', uid))) for el in account_ids]) - + return dict([(account_id, self.on_change_account_id(cr, uid, ids, account_id, user_id=context.get('user_id', uid), is_timesheet=True, context=context)) for account_id in account_ids]) class hr_attendance(osv.osv): @@ -550,7 +535,6 @@ class hr_timesheet_sheet_sheet_day(osv.osv): } _depends = { 'account.analytic.line': ['date', 'unit_amount'], - 'hr.analytic.timesheet': ['line_id', 'sheet_id'], 'hr.attendance': ['action', 'name', 'sheet_id'], } @@ -583,7 +567,7 @@ class hr_timesheet_sheet_sheet_day(osv.osv): FROM (( select - min(hrt.id) as id, + min(l.id) as id, p.tz as timezone, l.date::date as name, s.id as sheet_id, @@ -591,9 +575,8 @@ class hr_timesheet_sheet_sheet_day(osv.osv): 0 as orphan_attendances, 0.0 as total_attendance from - hr_analytic_timesheet hrt - JOIN account_analytic_line l ON l.id = hrt.line_id - LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = hrt.sheet_id + account_analytic_line l + LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = l.sheet_id JOIN hr_employee e ON s.employee_id = e.id JOIN resource_resource r ON e.resource_id = r.id LEFT JOIN res_users u ON r.user_id = u.id @@ -642,26 +625,23 @@ class hr_timesheet_sheet_sheet_account(osv.osv): _depends = { 'account.analytic.line': ['account_id', 'date', 'to_invoice', 'unit_amount', 'user_id'], - 'hr.analytic.timesheet': ['line_id'], 'hr_timesheet_sheet.sheet': ['date_from', 'date_to', 'user_id'], } def init(self, cr): cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as ( select - min(hrt.id) as id, + min(l.id) as id, l.account_id as name, s.id as sheet_id, sum(l.unit_amount) as total, l.to_invoice as invoice_rate from - hr_analytic_timesheet hrt - left join (account_analytic_line l + account_analytic_line l LEFT JOIN hr_timesheet_sheet_sheet s ON (s.date_to >= l.date AND s.date_from <= l.date - AND s.user_id = l.user_id)) - on (l.id = hrt.line_id) + AND s.user_id = l.user_id) group by l.account_id, s.id, l.to_invoice )""") diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml index aa5685d3d4a..daedf0686f0 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml @@ -42,9 +42,9 @@ <record model="ir.actions.act_window" id="act_hr_timesheet_sheet_sheet_2_hr_analytic_timesheet"> - <field name="context">{'search_default_sheet_id': [active_id]}</field> + <field name="context">{'search_default_sheet_id': [active_id], 'default_is_timesheet' : 1}</field> <field name="name">Timesheet Activities</field> - <field name="res_model">hr.analytic.timesheet</field> + <field name="res_model">account.analytic.line</field> <field name="src_model">hr_timesheet_sheet.sheet</field> </record> @@ -56,6 +56,23 @@ <field name="src_model">hr_timesheet_sheet.sheet</field> </record> + <record id="hr_timesheet_sheet_tree_simplified" model="ir.ui.view"> + <field name="name">hr.timesheet.sheet.tree</field> + <field name="model">hr_timesheet_sheet.sheet</field> + <field name="arch" type="xml"> + <tree colors="blue:state == 'draft';black:state in ('confirm','new');gray:state == 'done'" fonts="bold: message_unread == True" string="Timesheets"> + <field name="employee_id"/> + <field name="date_from"/> + <field name="date_to"/> + <field name="department_id" invisible="1"/> + <field name="total_attendance" widget="float_time" groups="base.group_hr_attendance"/> + <field name="total_timesheet" widget="float_time"/> + <field name="total_difference" widget="float_time" groups="base.group_hr_attendance"/> + <field name="state"/> + <field name="message_unread" invisible="1"/> + </tree> + </field> + </record> <record id="hr_timesheet_sheet_form" model="ir.ui.view"> <field name="name">hr.timesheet.sheet.form</field> @@ -107,10 +124,10 @@ </widget> </page> <page string="Details"> - <field context="{'employee_id': employee_id, 'user_id':user_id, 'timesheet_date_from': date_from, 'timesheet_date_to': date_to}" name="timesheet_ids" nolabel="1"> + <field context="{'employee_id': employee_id, 'user_id':user_id, 'timesheet_date_from': date_from, 'timesheet_date_to': date_to, 'default_is_timesheet' : 1}" name="timesheet_ids" nolabel="1"> <tree editable="top" string="Timesheet Activities"> <field name="date"/> - <field domain="[('type','in',['normal', 'contract']), ('state', '<>', 'close'),('invoice_on_timesheets','=',1)]" name="account_id" on_change="on_change_account_id(account_id, user_id)" context="{'default_use_timesheets': 1}"/> + <field domain="[('type','in',['normal', 'contract']), ('state', '!=', 'close'),('invoice_on_timesheets','=',1)]" name="account_id" on_change="on_change_account_id(account_id, user_id)" options="{'no_open': True, 'no_create': True}" context="{'default_use_timesheets': 1}"/> <field name="name"/> <field name="unit_amount" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" widget="float_time" string="Hours" sum="Hours"/> <field name="to_invoice" widget="selection"/> @@ -120,10 +137,11 @@ <field invisible="1" name="amount"/> <field invisible="1" name="general_account_id"/> <field invisible="1" name="user_id" required="1"/> + <field name="is_timesheet" invisible="1"/> </tree> <form string="Timesheet Activities"> <field name="date"/> - <field domain="[('type','=','normal'), ('state', '<>', 'close')]" name="account_id" on_change="on_change_account_id(account_id, user_id)"/> + <field domain="[('type','=','normal'), ('state', '!=', 'close')]" name="account_id" on_change="on_change_account_id(account_id, user_id)"/> <field name="name"/> <field name="unit_amount" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" widget="float_time"/> <field name="to_invoice"/> @@ -133,6 +151,7 @@ <field name="amount"/> <field name="general_account_id"/> <field name="user_id" required="1"/> + <field name="is_timesheet" invisible="1"/> </form> </field> </page> @@ -198,7 +217,7 @@ <field name="res_model">hr_timesheet_sheet.sheet</field> <field name="view_type">form</field> <field name="view_id" eval="False"/> - <field name="context">{'search_default_to_approve':1}</field> + <field name="context">{'search_default_to_approve':1, 'default_is_timesheet':1}</field> <field name="search_view_id" ref="view_hr_timesheet_sheet_filter"/> <field name="help" type="html"> <p class="oe_view_nocontent_create"> @@ -214,9 +233,15 @@ </field> </record> - <menuitem id="menu_timesheet_approvals" name="Approvals" parent="hr_attendance.timesheet_menu_root" sequence="6" groups="base.group_hr_user"/> - <menuitem action="act_hr_timesheet_sheet_form" id="menu_act_hr_timesheet_sheet_form" parent="menu_timesheet_approvals" - sequence="11" /> + <record model="ir.actions.act_window.view" id="action_open_timesheet_sheet_holidays_tree"> + <field name="sequence" eval="2"/> + <field name="view_mode">tree</field> + <field name="view_id" ref="hr_timesheet_sheet_tree_simplified"/> + <field name="act_window_id" ref="act_hr_timesheet_sheet_form"/> + </record> + + <menuitem action="act_hr_timesheet_sheet_form" id="menu_act_hr_timesheet_sheet_form" parent="hr_attendance.menu_hr_time_tracking" + sequence="11" groups="base.group_hr_user"/> <!-- Company inheritancy @@ -237,12 +262,12 @@ </record> <!-- - hr.analytic.timesheet inheritancy + account.analytic.line inheritancy --> <record id="hr_timesheet_line_form" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.form</field> - <field name="model">hr.analytic.timesheet</field> + <field name="name">account.analytic.line.form</field> + <field name="model">account.analytic.line</field> <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/> <field name="arch" type="xml"> <field name="date" position="after"> @@ -252,8 +277,8 @@ </record> <record id="hr_timesheet_line_search" model="ir.ui.view"> - <field name="name">hr.analytic.timesheet.search</field> - <field name="model">hr.analytic.timesheet</field> + <field name="name">account.analytic.line.search</field> + <field name="model">account.analytic.line</field> <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_search"/> <field name="arch" type="xml"> <field name="date" position="before"> @@ -324,24 +349,6 @@ res_model="hr_timesheet_sheet.sheet.day" src_model="hr_timesheet_sheet.sheet"/> - <record id="hr_timesheet_sheet_tree_simplified" model="ir.ui.view"> - <field name="name">hr.timesheet.sheet.tree</field> - <field name="model">hr_timesheet_sheet.sheet</field> - <field eval="10" name="priority"/> - <field name="arch" type="xml"> - <tree colors="blue:state == 'draft';black:state in ('confirm','new');gray:state == 'done'" fonts="bold: message_unread == True" string="Timesheets"> - <field name="employee_id"/> - <field name="date_from"/> - <field name="date_to"/> - <field name="department_id" invisible="1"/> - <field name="total_attendance" widget="float_time" groups="base.group_hr_attendance"/> - <field name="total_timesheet" widget="float_time"/> - <field name="total_difference" widget="float_time" groups="base.group_hr_attendance"/> - <field name="state"/> - <field name="message_unread" invisible="1"/> - </tree> - </field> - </record> <record id="hr_timesheet_day_tree" model="ir.ui.view"> <field name="name">hr.timesheet.day.tree</field> @@ -363,7 +370,7 @@ <field name="view_type">form</field> <field name="name">Timesheets</field> <field name="view_mode">tree,form</field> - <field name="context">{'search_default_employee_id': [active_id], 'default_employee_id': active_id}</field> + <field name="context">{'search_default_employee_id': [active_id], 'default_employee_id': active_id , 'default_is_timesheet' : 1}</field> </record> <record id="hr_timesheet_sheet_employee_extd_form" model="ir.ui.view"> diff --git a/addons/hr_timesheet_sheet/report/hr_timesheet_report.py b/addons/hr_timesheet_sheet/report/hr_timesheet_report.py index 6d41cc6443d..8aaea5d2606 100644 --- a/addons/hr_timesheet_sheet/report/hr_timesheet_report.py +++ b/addons/hr_timesheet_sheet/report/hr_timesheet_report.py @@ -52,11 +52,11 @@ class hr_timesheet_report(osv.osv): sum(total_attendance) / coalesce(sum(j.count),1) as total_attendance FROM hr_timesheet_sheet_sheet_day d left join ( SELECT - h.sheet_id, + a.sheet_id, a.date, count(*) - FROM account_analytic_line a inner join hr_analytic_timesheet h ON (h.line_id=a.id) - GROUP BY h.sheet_id, a.date + FROM account_analytic_line a + GROUP BY a.sheet_id, a.date ) j ON (d.sheet_id = j.sheet_id AND d.name = j.date) GROUP BY d.sheet_id, d.name ) @@ -73,7 +73,7 @@ class hr_timesheet_report(osv.osv): htss.state""" def _from(self): - return super(hr_timesheet_report, self)._from() + "left join hr_timesheet_sheet_sheet as htss ON (hat.sheet_id=htss.id) join totals as t on (t.sheet_id = hat.sheet_id and t.date = aal.date)" + return super(hr_timesheet_report, self)._from() + "left join hr_timesheet_sheet_sheet as htss ON (aal.sheet_id=htss.id) join totals as t on (t.sheet_id = aal.sheet_id and t.date = aal.date)" def _group_by(self): return super(hr_timesheet_report, self)._group_by() + """, diff --git a/addons/hr_timesheet_sheet/security/ir.model.access.csv b/addons/hr_timesheet_sheet/security/ir.model.access.csv index 2ad3c7d5ca2..d3185141612 100644 --- a/addons/hr_timesheet_sheet/security/ir.model.access.csv +++ b/addons/hr_timesheet_sheet/security/ir.model.access.csv @@ -3,5 +3,4 @@ access_hr_timesheet_sheet_sheet_user,hr_timesheet_sheet.sheet.user,model_hr_time access_hr_timesheet_sheet_sheet_system_employee,hr_timesheet_sheet.sheet.system.employee,model_hr_timesheet_sheet_sheet,base.group_user,1,1,1,0 access_hr_timesheet_sheet_sheet_day,hr_timesheet_sheet.sheet.day,model_hr_timesheet_sheet_sheet_day,base.group_hr_user,1,1,1,1 access_hr_timesheet_sheet_sheet_account,hr_timesheet_sheet.sheet.account,model_hr_timesheet_sheet_sheet_account,base.group_hr_user,1,1,1,1 -access_hr_analytic_timesheet_system_user,hr.analytic.timesheet.system.user,model_hr_analytic_timesheet,base.group_user,1,0,0,0 access_hr_timesheet_sheet_sheet_day,hr.timesheet.sheet.sheet.day.user,model_hr_timesheet_sheet_sheet_day,base.group_user,1,1,1,0 diff --git a/addons/hr_timesheet_sheet/static/src/js/timesheet.js b/addons/hr_timesheet_sheet/static/src/js/timesheet.js index 805333795e4..c99a3593d7e 100644 --- a/addons/hr_timesheet_sheet/static/src/js/timesheet.js +++ b/addons/hr_timesheet_sheet/static/src/js/timesheet.js @@ -107,9 +107,9 @@ var WeeklyTimesheet = form_common.FormWidget.extend(form_common.ReinitializeWidg var accounts; var account_names; var default_get; - return this.render_drop.add(new Model("hr.analytic.timesheet").call("default_get", [ - ['account_id','general_account_id', 'journal_id','date','name','user_id','product_id','product_uom_id','to_invoice','amount','unit_amount'], - new data.CompoundContext({'user_id': self.get('user_id')})]).then(function(result) { + return this.render_drop.add(new Model("account.analytic.line").call("default_get", [ + ['account_id','general_account_id', 'journal_id','date','name','user_id','product_id','product_uom_id','to_invoice','amount','unit_amount', 'is_timesheet'], + new data.CompoundContext({'user_id': self.get('user_id'), 'default_is_timesheet':true})]).then(function(result) { default_get = result; // calculating dates dates = []; @@ -132,7 +132,7 @@ var WeeklyTimesheet = form_common.FormWidget.extend(form_common.ReinitializeWidg var account_ids = _.map(_.keys(accounts), function(el) { return el === "false" ? false : Number(el); }); - return new Model("hr.analytic.timesheet").call("multi_on_change_account_id", [[], account_ids, + return new Model("account.analytic.line").call("multi_on_change_account_id", [[], account_ids, new data.CompoundContext({'user_id': self.get('user_id')})]).then(function(accounts_defaults) { accounts = _(accounts).chain().map(function(lines, account_id) { var account_defaults = _.extend({}, default_get, (accounts_defaults[account_id] || {}).value || {}); @@ -216,7 +216,7 @@ var WeeklyTimesheet = form_common.FormWidget.extend(form_common.ReinitializeWidg account.days[day_count].lines[0].unit_amount += num - self.sum_box(account, day_count); var product = (account.days[day_count].lines[0].product_id instanceof Array) ? account.days[day_count].lines[0].product_id[0] : account.days[day_count].lines[0].product_id; var journal = (account.days[day_count].lines[0].journal_id instanceof Array) ? account.days[day_count].lines[0].journal_id[0] : account.days[day_count].lines[0].journal_id; - self.defs.push(new Model("hr.analytic.timesheet").call("on_change_unit_amount", [[], product, account.days[day_count].lines[0].unit_amount, false, false, journal]).then(function(res) { + self.defs.push(new Model("account.analytic.line").call("on_change_unit_amount", [[], product, account.days[day_count].lines[0].unit_amount, false, false, journal]).then(function(res) { account.days[day_count].lines[0]['amount'] = res.value.amount || 0; self.display_totals(); self.sync(); @@ -271,7 +271,7 @@ var WeeklyTimesheet = form_common.FormWidget.extend(form_common.ReinitializeWidg return; } var ops = self.generate_o2m_value(); - new Model("hr.analytic.timesheet").call("multi_on_change_account_id", [[], [id], + new Model("account.analytic.line").call("multi_on_change_account_id", [[], [id], new data.CompoundContext({'user_id': self.get('user_id')})]).then(function(res) { res = res[id]; var def = _.extend({}, self.default_get, res.value, { diff --git a/addons/portal_project/security/ir.model.access.csv b/addons/portal_project/security/ir.model.access.csv index d2a7287e355..45998a38841 100644 --- a/addons/portal_project/security/ir.model.access.csv +++ b/addons/portal_project/security/ir.model.access.csv @@ -2,12 +2,10 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_project,project,project.model_project_project,base.group_portal,1,0,0,0 access_task,task,project.model_project_task,base.group_portal,1,0,0,0 access_task_type,task_type,project.model_project_task_type,base.group_portal,1,0,0,0 -access_task_work,task_work,project.model_project_task_work,base.group_portal,1,0,0,0 access_project_category,project_category,project.model_project_category,base.group_portal,1,0,0,0 access_account_analytic_account,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0 access_project_public,project,project.model_project_project,base.group_public,1,0,0,0 access_task_public,task,project.model_project_task,base.group_public,1,0,0,0 access_task_type_public,task_type,project.model_project_task_type,base.group_public,1,0,0,0 -access_task_work_public,task_work,project.model_project_task_work,base.group_public,1,0,0,0 access_project_category_public,project_category,project.model_project_category,base.group_public,1,0,0,0 access_account_analytic_account_public,account_analytic_account,analytic.model_account_analytic_account,base.group_public,1,0,0,0
\ No newline at end of file diff --git a/addons/project/project.py b/addons/project/project.py index af4789fd0b5..a02947c7387 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -112,73 +112,6 @@ class project(osv.osv): val['pricelist_id'] = pricelist_id return {'value': val} - def _get_projects_from_tasks(self, cr, uid, task_ids, context=None): - tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context) - project_ids = [task.project_id.id for task in tasks if task.project_id] - return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context) - - def _get_project_and_parents(self, cr, uid, ids, context=None): - """ return the project ids and all their parent projects """ - res = set(ids) - while ids: - cr.execute(""" - SELECT DISTINCT parent.id - FROM project_project project, project_project parent, account_analytic_account account - WHERE project.analytic_account_id = account.id - AND parent.analytic_account_id = account.parent_id - AND project.id IN %s - """, (tuple(ids),)) - ids = [t[0] for t in cr.fetchall()] - res.update(ids) - return list(res) - - def _get_project_and_children(self, cr, uid, ids, context=None): - """ retrieve all children projects of project ids; - return a dictionary mapping each project to its parent project (or None) - """ - res = dict.fromkeys(ids, None) - while ids: - cr.execute(""" - SELECT project.id, parent.id - FROM project_project project, project_project parent, account_analytic_account account - WHERE project.analytic_account_id = account.id - AND parent.analytic_account_id = account.parent_id - AND parent.id IN %s - """, (tuple(ids),)) - dic = dict(cr.fetchall()) - res.update(dic) - ids = dic.keys() - return res - - def _progress_rate(self, cr, uid, ids, names, arg, context=None): - child_parent = self._get_project_and_children(cr, uid, ids, context) - # compute planned_hours, total_hours, effective_hours specific to each project - cr.execute(""" - SELECT project_id, COALESCE(SUM(planned_hours), 0.0), - COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0) - FROM project_task - LEFT JOIN project_task_type ON project_task.stage_id = project_task_type.id - WHERE project_task.project_id IN %s AND project_task_type.fold = False - GROUP BY project_id - """, (tuple(child_parent.keys()),)) - # aggregate results into res - res = dict([(id, {'planned_hours':0.0, 'total_hours':0.0, 'effective_hours':0.0}) for id in ids]) - for id, planned, total, effective in cr.fetchall(): - # add the values specific to id to all parent projects of id in the result - while id: - if id in ids: - res[id]['planned_hours'] += planned - res[id]['total_hours'] += total - res[id]['effective_hours'] += effective - id = child_parent[id] - # compute progress rates - for id in ids: - if res[id]['total_hours']: - res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2) - else: - res[id]['progress_rate'] = 0.0 - return res - def unlink(self, cr, uid, ids, context=None): alias_ids = [] mail_alias = self.pool.get('mail.alias') @@ -284,26 +217,6 @@ class project(osv.osv): 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"), - 'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.", - store = { - 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), - 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20), - }), - 'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.", - store = { - 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), - 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20), - }), - 'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.", - store = { - 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), - 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20), - }), - 'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.", - store = { - 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), - 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20), - }), 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('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)]}), 'task_count': fields.function(_task_count, type='integer', string="Tasks",), @@ -759,12 +672,6 @@ class task(osv.osv): res[task.id] = False return res - def _get_task(self, cr, uid, ids, context=None): - result = {} - for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context): - if work.task_id: result[work.task_id.id] = True - return result.keys() - _columns = { 'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."), 'name': fields.char('Task Summary', track_visibility='onchange', size=128, required=True, select=True), @@ -792,31 +699,10 @@ class task(osv.osv): 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'), 'notes': fields.text('Notes'), 'planned_hours': fields.float('Initially Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'), - 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.", - store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10), - 'project.task.work': (_get_task, ['hours'], 10), - }), 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."), - 'total_hours': fields.function(_hours_get, string='Total', multi='hours', help="Computed as: Time Spent + Remaining Time.", - store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10), - 'project.task.work': (_get_task, ['hours'], 10), - }), - 'progress': fields.function(_hours_get, string='Working Time Progress (%)', multi='hours', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time", - store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10), - 'project.task.work': (_get_task, ['hours'], 10), - }), - 'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='hours', help="Computed as difference between planned hours by the project manager and the total hours of the task.", - store = { - 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10), - 'project.task.work': (_get_task, ['hours'], 10), - }), 'user_id': fields.many2one('res.users', 'Assigned to', select=True, track_visibility='onchange'), 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'), 'partner_id': fields.many2one('res.partner', 'Customer'), - 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'), 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'), 'company_id': fields.many2one('res.company', 'Company'), 'id': fields.integer('ID', readonly=True), @@ -829,7 +715,6 @@ class task(osv.osv): 'date_last_stage_update': fields.datetime.now, 'kanban_state': 'normal', 'priority': '0', - 'progress': 0, 'sequence': 10, 'active': True, 'user_id': lambda obj, cr, uid, ctx=None: uid, @@ -889,6 +774,10 @@ class task(osv.osv): def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): users_obj = self.pool.get('res.users') if context is None: context = {} + # read uom as admin to avoid access rights issues, e.g. for portal/share users, + # this should be safe (no context passed to avoid side-effects) + obj_tm = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id + tm = obj_tm and obj_tm.name or 'Hours' res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu) @@ -984,6 +873,29 @@ class task(osv.osv): new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context)) return new_attachment_ids + def _get_effective_hours(self, task): + # this method is override in project_timesheet modules. + return 0.0 + + def _prepare_delegate_values(self, cr, uid, ids, delegate_data, context=None): + delegate_values = {} + for task in self.browse(cr, uid, ids, context=context): + delegate_values[task.id] = { + 'name': delegate_data['name'], + 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False, + 'stage_id': delegate_data.get('stage_id') and delegate_data.get('stage_id')[0] or False, + 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False, + 'planned_hours': delegate_data['planned_hours'] or 0.0, + 'parent_ids': [(6, 0, [task.id])], + 'description': delegate_data['new_task_description'] or '', + 'child_ids': [], + 'remaining_hours': delegate_data['planned_hours_me'], + # The values below will be written on the old task. + 'task_planned_hours': delegate_data['planned_hours_me'], + 'task_name': delegate_data['prefix'] or '' + } + return delegate_values + def do_delegate(self, cr, uid, ids, delegate_data=None, context=None): """ Delegate Task to another users. @@ -992,24 +904,25 @@ class task(osv.osv): delegate_data = {} assert delegate_data['user_id'], _("Delegated User should be specified") delegated_tasks = {} + vals = self._prepare_delegate_values(cr, uid, ids, delegate_data, context) for task in self.browse(cr, uid, ids, context=context): + delegated_task_id = self.copy(cr, uid, task.id, { - 'name': delegate_data['name'], - 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False, - 'stage_id': delegate_data.get('stage_id') and delegate_data.get('stage_id')[0] or False, - 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False, - 'planned_hours': delegate_data['planned_hours'] or 0.0, - 'parent_ids': [(6, 0, [task.id])], - 'description': delegate_data['new_task_description'] or '', - 'child_ids': [], - 'work_ids': [] + 'name': vals[task.id]['name'], + 'project_id': vals[task.id]['project_id'], + 'stage_id': vals[task.id]['stage_id'], + 'user_id': vals[task.id]['user_id'], + 'planned_hours': vals[task.id]['planned_hours'], + 'parent_ids': vals[task.id]['parent_ids'], + 'description': vals[task.id]['description'], + 'child_ids': vals[task.id]['child_ids'], }, context=context) self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context) - newname = delegate_data['prefix'] or '' + task.write({ - 'remaining_hours': delegate_data['planned_hours_me'], - 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0), - 'name': newname, + 'remaining_hours': vals[task.id]['remaining_hours'], + 'planned_hours': vals[task.id]['task_planned_hours'], + 'name': vals[task.id]['task_name'] }, context=context) delegated_tasks[task.id] = delegated_task_id return delegated_tasks @@ -1100,6 +1013,9 @@ class task(osv.osv): res = super(task, self).unlink(cr, uid, ids, context) return res + def _get_total_hours(self, task): + return self._get_effective_hours(task) + task.remaining_hours + def _generate_task(self, cr, uid, tasks, ident=4, context=None): context = context or {} result = "" @@ -1110,7 +1026,7 @@ class task(osv.osv): result += ''' %sdef Task_%s(): %s todo = \"%.2fH\" -%s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours) +%s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident, self._get_total_hours(task)) start = [] for t2 in task.parent_ids: start.append("up.Task_%s.end" % (t2.id,)) @@ -1192,50 +1108,6 @@ class task(osv.osv): pass return super(task, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context) -class project_work(osv.osv): - _name = "project.task.work" - _description = "Project Task Work" - _columns = { - 'name': fields.char('Work summary'), - 'date': fields.datetime('Date', select="1"), - 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"), - 'hours': fields.float('Time Spent'), - 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"), - 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True) - } - - _defaults = { - 'user_id': lambda obj, cr, uid, context: uid, - 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S') - } - - _order = "date desc" - def create(self, cr, uid, vals, context=None): - if 'hours' in vals and (not vals['hours']): - vals['hours'] = 0.00 - if 'task_id' in vals: - cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id'])) - self.pool.get('project.task').invalidate_cache(cr, uid, ['remaining_hours'], [vals['task_id']], context=context) - return super(project_work,self).create(cr, uid, vals, context=context) - - def write(self, cr, uid, ids, vals, context=None): - if 'hours' in vals and (not vals['hours']): - vals['hours'] = 0.00 - if 'hours' in vals: - task_obj = self.pool.get('project.task') - for work in self.browse(cr, uid, ids, context=context): - cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id)) - task_obj.invalidate_cache(cr, uid, ['remaining_hours'], [work.task_id.id], context=context) - return super(project_work,self).write(cr, uid, ids, vals, context) - - def unlink(self, cr, uid, ids, context=None): - task_obj = self.pool.get('project.task') - for work in self.browse(cr, uid, ids): - cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id)) - task_obj.invalidate_cache(cr, uid, ['remaining_hours'], [work.task_id.id], context=context) - return super(project_work,self).unlink(cr, uid, ids, context=context) - - class account_analytic_account(osv.osv): _inherit = 'account.analytic.account' _description = 'Analytic Account' diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index 09f17030249..8e78ee03a18 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -158,10 +158,6 @@ attrs="{'readonly':[('state','in',['close', 'cancelled'])]}" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_manager']}"/> <field name="privacy_visibility" widget="radio"/> - <field name="planned_hours" widget="float_time" - groups="project.group_time_work_estimation_tasks"/> - <field name="effective_hours" widget="float_time" - groups="project.group_time_work_estimation_tasks"/> <field name="resource_calendar_id" groups="project.group_time_work_estimation_tasks"/> </group> @@ -230,10 +226,6 @@ <field name="user_id" string="Project Manager"/> <field name="partner_id" string="Contact"/> <field name="parent_id" string="Parent" invisible="1"/> - <field name="planned_hours" widget="float_time"/> - <field name="total_hours" widget="float_time"/> - <field name="effective_hours" widget="float_time"/> - <field name="progress_rate" widget="progressbar"/> <field name="state"/> </tree> </field> @@ -244,8 +236,6 @@ <field name="model">project.project</field> <field name="arch" type="xml"> <kanban class="oe_background_grey"> - <field name="effective_hours"/> - <field name="planned_hours"/> <field name="name"/> <field name="members"/> <field name="use_tasks"/> @@ -348,35 +338,6 @@ <field name="view_id" ref="view_project"/> </record> - <record id="view_task_work_form" model="ir.ui.view"> - <field name="name">project.task.work.form</field> - <field name="model">project.task.work</field> - <field name="arch" type="xml"> - <form string="Task Work"> - <group col="4"> - <field colspan="4" name="name"/> - <field name="hours" widget="float_time"/> - <field name="date"/> - <field name="user_id" options='{"no_open": True}' context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_partner_manager', 'project.group_project_user']}"/> - <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/> - </group> - </form> - </field> - </record> - - <record id="view_task_work_tree" model="ir.ui.view"> - <field name="name">project.task.work.tree</field> - <field name="model">project.task.work</field> - <field name="arch" type="xml"> - <tree editable="top" string="Task Work"> - <field name="date"/> - <field name="name"/> - <field name="hours" widget="float_time"/> - <field name="user_id" context="{'default_groups_ref': ['base.group_user', 'project.group_project_user']}"/> - </tree> - </field> - </record> - <!-- Task --> <record id="view_task_form2" model="ir.ui.view"> @@ -410,36 +371,16 @@ context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_user']}"/> <field name="planned_hours" widget="float_time" groups="project.group_time_work_estimation_tasks" - on_change="onchange_planned(planned_hours, effective_hours)"/> + on_change="onchange_planned(planned_hours)"/> </group> <group> <field name="date_deadline"/> <field name="categ_ids" widget="many2many_tags"/> - <field name="progress" widget="progressbar" - groups="project.group_time_work_estimation_tasks"/> </group> </group> <notebook> - <page string="Description"> + <page name="description_page" string="Description"> <field name="description" type="html"/> - <field name="work_ids" groups="project.group_tasks_work_on_tasks"> - <tree string="Task Work" editable="top"> - <field name="name"/> - <field name="hours" widget="float_time" sum="Spent Hours"/> - <field name="date"/> - <field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_user']}"/> - </tree> - </field> - <group> - <group class="oe_subtotal_footer oe_right" name="project_hours" groups="project.group_time_work_estimation_tasks"> - <field name="effective_hours" widget="float_time"/> - <label for="remaining_hours" string="Remaining" groups="project.group_time_work_estimation_tasks"/> - <div> - <field name="remaining_hours" widget="float_time" groups="project.group_time_work_estimation_tasks"/> - </div> - <field name="total_hours" widget="float_time" class="oe_subtotal_footer_separator"/> - </group> - </group> <div class="oe_clear"/> </page> <page string="Delegation" groups="project.group_delegate_task"> @@ -449,13 +390,10 @@ <field name="parent_ids"/> <separator string="Delegated tasks"/> <field name="child_ids"> - <tree string="Delegated tasks"> + <tree name="delegated_task" string="Delegated tasks"> <field name="name"/> <field name="user_id"/> <field name="stage_id"/> - <field name="effective_hours" widget="float_time"/> - <field name="progress" widget="progressbar"/> - <field name="remaining_hours" widget="float_time"/> <field name="date_deadline"/> </tree> </field> @@ -512,14 +450,6 @@ <ul class="oe_dropdown_menu"> <t t-if="widget.view.is_action_enabled('edit')"><li><a type="edit">Edit Task</a></li></t> <t t-if="widget.view.is_action_enabled('delete')"><li><a type="delete">Delete</a></li></t> - <li> - <ul class="oe_kanban_project_times" groups="project.group_time_work_estimation_tasks"> - <li><a name="set_remaining_time_1" type="object" class="oe_kanban_button">1</a></li> - <li><a name="set_remaining_time_2" type="object" class="oe_kanban_button">2</a></li> - <li><a name="set_remaining_time_5" type="object" class="oe_kanban_button">5</a></li> - <li><a name="set_remaining_time_10" type="object" class="oe_kanban_button">10</a></li> - </ul> - </li> <br/> <li><ul class="oe_kanban_colorpicker" data-field="color"/></li> </ul> @@ -567,15 +497,11 @@ <field name="project_id" invisible="context.get('user_invisible', False)"/> <field name="user_id" invisible="context.get('user_invisible', False)"/> <field name="delegated_user_id" invisible="context.get('show_delegated', True)"/> - <field name="total_hours" invisible="1"/> <field name="planned_hours" invisible="context.get('set_visible',False)" groups="project.group_time_work_estimation_tasks"/> - <field name="effective_hours" widget="float_time" sum="Spent Hours" invisible="1"/> - <field name="remaining_hours" widget="float_time" sum="Remaining Hours" on_change="onchange_remaining(remaining_hours,planned_hours)" invisible="context.get('set_visible',False)" groups="project.group_time_work_estimation_tasks"/> <field name="date_deadline" invisible="context.get('deadline_visible',True)"/> <field name="stage_id" invisible="context.get('set_visible',False)"/> <field name="date_start" groups="base.group_no_one"/> <field name="date_end" groups="base.group_no_one"/> - <field name="progress" widget="progressbar" invisible="context.get('set_visible',False)"/> </tree> </field> </record> diff --git a/addons/project/report/project_report.py b/addons/project/report/project_report.py index dd7a558feba..e607b9729b4 100644 --- a/addons/project/report/project_report.py +++ b/addons/project/report/project_report.py @@ -36,12 +36,6 @@ class report_project_task_user(osv.osv): 'date_deadline': fields.date('Deadline', readonly=True), 'date_last_stage_update': fields.datetime('Last Stage Update', readonly=True), 'project_id': fields.many2one('project.project', 'Project', readonly=True), - 'hours_planned': fields.float('Planned Hours', readonly=True), - 'hours_effective': fields.float('Effective Hours', readonly=True), - 'hours_delay': fields.float('Avg. Plan.-Eff.', readonly=True), - 'remaining_hours': fields.float('Remaining Hours', readonly=True), - 'progress': fields.float('Progress', readonly=True, group_operator='avg'), - 'total_hours': fields.float('Total Hours', readonly=True), 'closing_days': fields.float('Days to Close', digits=(16,2), readonly=True, group_operator="avg", help="Number of Days to close the task"), 'opening_days': fields.float('Days to Assign', digits=(16,2), readonly=True, group_operator="avg", @@ -57,11 +51,9 @@ class report_project_task_user(osv.osv): } _order = 'name desc, project_id' - def init(self, cr): - tools.sql.drop_view_if_exists(cr, 'report_project_task_user') - cr.execute(""" - CREATE view report_project_task_user as - SELECT + def _select(self): + select_str = """ + SELECT (select 1 ) AS nbr, t.id as id, t.date_start as date_start, @@ -70,32 +62,23 @@ class report_project_task_user(osv.osv): t.date_deadline as date_deadline, abs((extract('epoch' from (t.write_date-t.date_start)))/(3600*24)) as no_of_days, t.user_id, - progress as progress, t.project_id, - t.effective_hours as hours_effective, t.priority, t.name as name, t.company_id, t.partner_id, t.stage_id as stage_id, t.kanban_state as state, - remaining_hours as remaining_hours, - total_hours as total_hours, - t.delay_hours as hours_delay, - planned_hours as hours_planned, (extract('epoch' from (t.write_date-t.create_date)))/(3600*24) as closing_days, (extract('epoch' from (t.date_start-t.create_date)))/(3600*24) as opening_days, (extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days - FROM project_task t - WHERE t.active = 'true' + """ + return select_str + + def _group_by(self): + group_by_str = """ GROUP BY t.id, - remaining_hours, - t.effective_hours, - progress, - total_hours, - planned_hours, - hours_delay, create_date, write_date, date_start, @@ -109,4 +92,15 @@ class report_project_task_user(osv.osv): t.company_id, t.partner_id, stage_id - """) + """ + return group_by_str + + def init(self, cr): + tools.sql.drop_view_if_exists(cr, 'report_project_task_user') + cr.execute(""" + CREATE view report_project_task_user as + %s + FROM project_task t + WHERE t.active = 'true' + %s + """% (self._select(), self._group_by())) diff --git a/addons/project/res_config_view.xml b/addons/project/res_config_view.xml index 684ee49cfca..05c5e4bc42b 100644 --- a/addons/project/res_config_view.xml +++ b/addons/project/res_config_view.xml @@ -57,7 +57,7 @@ <label for="time_unit"/> <field name="time_unit" class="oe_inline" domain="[('category_id.name','=','Working Time')]"/> </div> - <div> + <div attrs="{'invisible': [('module_project_timesheet','=',False)]}"> <field name="group_tasks_work_on_tasks" class="oe_inline"/> <label for="group_tasks_work_on_tasks" /> </div> diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 28ac60f87bd..40541c94a77 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -6,7 +6,6 @@ access_account_analytic_account,account.analytic.account,analytic.model_account_ access_project_task_type_user,project.task.type user,model_project_task_type,project.group_project_user,1,0,0,0 access_project_task_type_manager,project.task.type manager,model_project_task_type,project.group_project_manager,1,1,1,1 access_project_task,project.task,model_project_task,project.group_project_user,1,1,1,1 -access_project_task_work,project.task.work,model_project_task_work,project.group_project_user,1,1,1,1 access_report_project_task_user,report.project.task.user,model_report_project_task_user,project.group_project_manager,1,1,1,1 access_partner_task user,base.res.partner user,base.model_res_partner,project.group_project_user,1,0,0,0 access_task_on_partner,project.task on partners,model_project_task,base.group_user,1,0,0,0 diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index fa22f9cd888..0f32f60a1f1 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -158,16 +158,6 @@ class project_issue(osv.Model): return res - def _hours_get(self, cr, uid, ids, field_names, args, context=None): - task_pool = self.pool.get('project.task') - res = {} - for issue in self.browse(cr, uid, ids, context=context): - progress = 0.0 - if issue.task_id: - progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress'] - res[issue.id] = {'progress' : progress} - return res - def _can_escalate(self, cr, uid, ids, field_name, arg, context=None): res = {} for issue in self.browse(cr, uid, ids, context=context): @@ -183,21 +173,6 @@ class project_issue(osv.Model): return {'value': {'partner_id': project.partner_id.id}} return {} - def _get_issue_task(self, cr, uid, ids, context=None): - issues = [] - issue_pool = self.pool.get('project.issue') - for task in self.pool.get('project.task').browse(cr, uid, ids, context=context): - issues += issue_pool.search(cr, uid, [('task_id','=',task.id)]) - return issues - - def _get_issue_work(self, cr, uid, ids, context=None): - issues = [] - issue_pool = self.pool.get('project.issue') - for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context): - if work.task_id: - issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)]) - return issues - _columns = { 'id': fields.integer('ID', readonly=True), 'name': fields.char('Issue', required=True), @@ -257,12 +232,6 @@ class project_issue(osv.Model): 'date_action_last': fields.datetime('Last Action', readonly=1), 'date_action_next': fields.datetime('Next Action', readonly=1), 'can_escalate': fields.function(_can_escalate, type='boolean', string='Can Escalate'), - 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.", - store = { - 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10), - 'project.task': (_get_issue_task, ['work_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10), - 'project.task.work': (_get_issue_work, ['hours'], 10), - }), } _defaults = { diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index 78d49bee9ad..d09319bcf3c 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -44,7 +44,6 @@ <label for="task_id" groups="base.group_user"/> <div groups="base.group_user"> <field name="task_id" on_change="onchange_task_id(task_id)" class="oe_inline" context="{'default_project_id':project_id}"/> - <field name="progress" widget="progressbar" attrs="{'invisible':[('task_id','=',False)]}" class="oe_inline"/> </div> </group> </group> @@ -91,7 +90,6 @@ <field name="priority" string="Priority" groups="base.group_user"/> <field name="create_date"/> <field name="user_id"/> - <field name="progress" widget="progressbar" attrs="{'invisible':[('task_id','=',False)]}"/> <field name="stage_id" widget="selection" readonly="1"/> <field name="categ_ids" invisible="1"/> <field name="task_id" invisible="1"/> diff --git a/addons/project_issue_sheet/__openerp__.py b/addons/project_issue_sheet/__openerp__.py index eb264d865b3..38003c358f0 100644 --- a/addons/project_issue_sheet/__openerp__.py +++ b/addons/project_issue_sheet/__openerp__.py @@ -35,6 +35,7 @@ Worklogs can be maintained to signify number of hours spent by users to handle a 'depends': [ 'project_issue', 'hr_timesheet_sheet', + 'project_timesheet', ], 'data': [ 'project_issue_sheet_view.xml', diff --git a/addons/project_issue_sheet/project_issue_sheet.py b/addons/project_issue_sheet/project_issue_sheet.py index ef810db095e..7b298ded6d3 100644 --- a/addons/project_issue_sheet/project_issue_sheet.py +++ b/addons/project_issue_sheet/project_issue_sheet.py @@ -25,8 +25,23 @@ from openerp.tools.translate import _ class project_issue(osv.osv): _inherit = 'project.issue' _description = 'project issue' + + def _hours_get(self, cr, uid, ids, field_names, args, context=None): + res = {} + for issue in self.browse(cr, uid, ids, context=context): + res[issue.id] = { 'progress' : issue.task_id.progress or 0.0 } + return res + + def _get_issue_task(self, cr, uid, task_ids, context=None): + return self.pool['project.issue'].search(cr, uid, [('task_id', 'in', task_ids)], context=context) + _columns = { - 'timesheet_ids': fields.one2many('hr.analytic.timesheet', 'issue_id', 'Timesheets'), + 'progress': fields.function(_hours_get, string='Progress (%)', multi='line_id', group_operator="avg", help="Computed as: Time Spent / Total Time.", + store = { + 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10), + 'project.task': (_get_issue_task, ['progress'], 10), + }), + 'timesheet_ids': fields.one2many('account.analytic.line', 'issue_id', 'Timesheets'), 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'), } @@ -63,14 +78,5 @@ class account_analytic_line(osv.osv): _inherit = 'account.analytic.line' _description = 'account analytic line' _columns = { - 'create_date' : fields.datetime('Create Date', readonly=True), - } - - -class hr_analytic_issue(osv.osv): - - _inherit = 'hr.analytic.timesheet' - _description = 'hr analytic timesheet' - _columns = { 'issue_id' : fields.many2one('project.issue', 'Issue'), } diff --git a/addons/project_issue_sheet/project_issue_sheet_view.xml b/addons/project_issue_sheet/project_issue_sheet_view.xml index 69dc2b41776..7d2eddb6361 100644 --- a/addons/project_issue_sheet/project_issue_sheet_view.xml +++ b/addons/project_issue_sheet/project_issue_sheet_view.xml @@ -6,6 +6,9 @@ <field name="model">project.issue</field> <field name="inherit_id" ref="project_issue.project_issue_form_view" /> <field name="arch" type="xml"> + <field name="task_id" position="after"> + <field name="progress" widget="progressbar" attrs="{'invisible':[('task_id','=',False)]}" class="oe_inline"/> + </field> <field name="project_id" position="attributes"> <attribute name="on_change">on_change_project(project_id)</attribute> </field> @@ -15,26 +18,39 @@ on_change='on_change_account_id(analytic_account_id)'/> </field> <xpath expr="//notebook/page[@name='extra_info']" position="before"> - <page string="Worklogs"> - <field name="timesheet_ids" colspan="4" nolabel="1" context="{'default_user_id' : uid, 'default_account_id' : analytic_account_id}" + <page string="Timesheets"> + <field name="timesheet_ids" colspan="4" nolabel="1" context="{'default_user_id' : uid, 'default_account_id' : analytic_account_id, 'default_is_timesheet':1}" groups="base.group_user"> - <tree editable="top" string="Timesheets"> - <field name="name"/> - <field name="unit_amount" string="Duration" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)" widget="float_time"/> - <field name="account_id" invisible="0" domain="[('partner_id', '=', parent.partner_id)]" on_change="on_change_account_id(account_id)" groups="analytic.group_analytic_accounting"/> + <tree editable="top" string="Timesheet Activities"> <field name="date"/> - <field name="user_id"/> - <field invisible="1" name="journal_id"/> - <field invisible="1" name="product_id"/> - <field invisible="1" name="product_uom_id" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id)"/> - <field invisible="1" name="amount"/> - <field invisible="1" name="general_account_id"/> - <field invisible="1" name="to_invoice"/> + <field name="user_id" required="1"/> + <field name="name"/> + <field name="account_id" invisible="0" domain="[('partner_id', '=', parent.partner_id)]" on_change="on_change_account_id(account_id, context)" groups="analytic.group_analytic_accounting"/> + <field name="unit_amount" string="Duration" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id, context)" widget="float_time"/> + <field name="to_invoice" invisible="1"/> + <field name="product_uom_id" invisible="1" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id, context)"/> + <field name="journal_id" invisible="1"/> + <field name="amount" invisible="1"/> + <field name="general_account_id" invisible="1"/> + <field name="product_id" invisible="1"/> + <field name="is_timesheet" invisible="1"/> </tree> </field> </page> </xpath> </field> </record> + + <record id="project_issue_tree_view_inherited" model="ir.ui.view"> + <field name="name">Project Issue Tracker Tree Inherited</field> + <field name="model">project.issue</field> + <field name="inherit_id" ref="project_issue.project_issue_tree_view" /> + <field name="arch" type="xml"> + <field name="user_id" position="after"> + <field name="progress" widget="progressbar" invisible="context.get('set_visible',False)"/> + </field> + </field> + </record> + </data> </openerp> diff --git a/addons/project_issue_sheet/security/ir.model.access.csv b/addons/project_issue_sheet/security/ir.model.access.csv index 3273f1f4247..d2b3808f0ae 100644 --- a/addons/project_issue_sheet/security/ir.model.access.csv +++ b/addons/project_issue_sheet/security/ir.model.access.csv @@ -1,9 +1,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_timesheet_sheet_sheet_project_user,hr_timesheet_sheet.sheet user,hr_timesheet_sheet.model_hr_timesheet_sheet_sheet,project.group_project_user,1,1,1,0
-access_hr_analytic_timesheet_project_user,hr_analytic_timesheet user,hr_timesheet_sheet.model_hr_analytic_timesheet,project.group_project_user,1,1,1,0
-access_hr_analytic_timesheet_project_manager,hr_analytic_timesheet manager,hr_timesheet_sheet.model_hr_analytic_timesheet,project.group_project_manager,1,0,0,0
+access_account_analytic_line_project_user,account_analytic_line user,hr_timesheet_sheet.model_account_analytic_line,project.group_project_user,1,1,1,0
+access_account_analytic_line_project_manager,account_analytic_line manager,hr_timesheet_sheet.model_account_analytic_line,project.group_project_manager,1,0,0,0
access_hr_timesheet_sheet_sheet_project_manager,hr_timesheet_sheet.sheet manager,hr_timesheet_sheet.model_hr_timesheet_sheet_sheet,project.group_project_manager,1,0,0,0
-access_hr_analytic_timesheet_project_manager,account.analytic.line.timesheet.project.manager,account.model_account_analytic_line,project.group_project_manager,1,1,1,1
+access_account_analytic_line_project_manager,account.analytic.line.timesheet.project.manager,account.model_account_analytic_line,project.group_project_manager,1,1,1,1
access_account_analytic_journal_project_manager,account.analytic.journal.project.manager,analytic.model_account_analytic_journal,project.group_project_manager,1,1,1,1
access_account_fiscalyear_project_manager,account.fiscalyear.project.manager,account.model_account_fiscalyear,project.group_project_manager,1,1,1,1
access_hr_timesheet_invoice_factor_project_manager,hr_timesheet_invoice.factor.project.manager,hr_timesheet_invoice.model_hr_timesheet_invoice_factor,project.group_project_manager,1,1,1,1
diff --git a/addons/project_timesheet/__init__.py b/addons/project_timesheet/__init__.py index 53a5e438263..709a7fd9da2 100644 --- a/addons/project_timesheet/__init__.py +++ b/addons/project_timesheet/__init__.py @@ -20,3 +20,4 @@ ############################################################################## import project_timesheet +import report diff --git a/addons/project_timesheet/__openerp__.py b/addons/project_timesheet/__openerp__.py index 4327d7a1871..87dd97ab532 100644 --- a/addons/project_timesheet/__openerp__.py +++ b/addons/project_timesheet/__openerp__.py @@ -33,16 +33,17 @@ with the effect of creating, editing and deleting either ways. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/project-management', - 'depends': ['resource', 'project', 'hr_timesheet', 'hr_timesheet_invoice', 'account_analytic_analysis', 'procurement'], + 'images': ['images/invoice_task_work.jpeg', 'images/my_timesheet.jpeg', 'images/working_hour.jpeg'], + 'depends': ['resource', 'project', 'account_analytic_analysis', 'procurement'], 'data': [ 'security/ir.model.access.csv', 'security/project_timesheet_security.xml', - 'project_timesheet_view.xml', + 'report/project_report_view.xml', + 'project_timesheet_view.xml' ], 'demo': ['project_timesheet_demo.xml'], 'test': [ 'test/worktask_entry_to_timesheetline_entry.yml', - 'test/work_timesheet.yml', ], 'installable': True, 'auto_install': True, diff --git a/addons/project_timesheet/project_timesheet.py b/addons/project_timesheet/project_timesheet.py index c036f4887d1..dada9b17f33 100644 --- a/addons/project_timesheet/project_timesheet.py +++ b/addons/project_timesheet/project_timesheet.py @@ -20,6 +20,7 @@ ############################################################################## import time import datetime +from dateutil.relativedelta import relativedelta from openerp.osv import fields, osv from openerp import tools @@ -29,6 +30,73 @@ from openerp.exceptions import UserError class project_project(osv.osv): _inherit = 'project.project' + def _get_project_and_children(self, cr, uid, ids, context=None): + """ retrieve all children projects of project ids; + return a dictionary mapping each project to its parent project (or None) + """ + res = dict.fromkeys(ids, None) + while ids: + cr.execute(""" + SELECT project.id, parent.id + FROM project_project project, project_project parent, account_analytic_account account + WHERE project.analytic_account_id = account.id + AND parent.analytic_account_id = account.parent_id + AND parent.id IN %s + """, (tuple(ids),)) + dic = dict(cr.fetchall()) + res.update(dic) + ids = dic.keys() + return res + + def _progress_rate(self, cr, uid, ids, names, arg, context=None): + child_parent = self._get_project_and_children(cr, uid, ids, context) + # compute planned_hours, total_hours, effective_hours specific to each project + cr.execute(""" + SELECT project_id, COALESCE(SUM(planned_hours), 0.0), + COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0) + FROM project_task + LEFT JOIN project_task_type ON project_task.stage_id = project_task_type.id + WHERE project_task.project_id IN %s AND project_task_type.fold = False + GROUP BY project_id + """, (tuple(child_parent.keys()),)) + # aggregate results into res + res = dict([(id, {'planned_hours': 0.0, 'total_hours': 0.0, 'effective_hours': 0.0}) for id in ids]) + for id, planned, total, effective in cr.fetchall(): + # add the values specific to id to all parent projects of id in the result + while id: + if id in ids: + res[id]['planned_hours'] += planned + res[id]['total_hours'] += total + res[id]['effective_hours'] += effective + id = child_parent[id] + # compute progress rates + for id in ids: + if res[id]['total_hours']: + res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2) + else: + res[id]['progress_rate'] = 0.0 + return res + + def _get_projects_from_tasks(self, cr, uid, task_ids, context=None): + tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context) + project_ids = [task.project_id.id for task in tasks if task.project_id] + return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context) + + def _get_project_and_parents(self, cr, uid, ids, context=None): + """ return the project ids and all their parent projects """ + res = set(ids) + while ids: + cr.execute(""" + SELECT DISTINCT parent.id + FROM project_project project, project_project parent, account_analytic_account account + WHERE project.analytic_account_id = account.id + AND parent.analytic_account_id = account.parent_id + AND project.id IN %s + """, (tuple(ids),)) + ids = [t[0] for t in cr.fetchall()] + res.update(ids) + return list(res) + 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,7 +107,30 @@ 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 - + + _columns = { + 'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.", + store = { + 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), + 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'timesheet_ids', 'stage_id'], 20), + }), + 'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.", + store = { + 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), + 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'timesheet_ids', 'stage_id'], 20), + }), + 'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.", + store = { + 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), + 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'timesheet_ids', 'stage_id'], 20), + }), + 'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.", + store = { + 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10), + 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'timesheet_ids', 'stage_id'], 20), + }), + } + _defaults = { 'invoice_on_timesheets': True, } @@ -53,6 +144,7 @@ class project_project(osv.osv): view_context = { 'search_default_account_id': [project.analytic_account_id.id], 'default_account_id': project.analytic_account_id.id, + 'default_is_timesheet':True } help = _("""<p class="oe_view_nocontent_create">Record your timesheets for the project '%s'.</p>""") % (project.name,) try: @@ -70,219 +162,72 @@ class project_project(osv.osv): result['help'] = help return result +class task(osv.osv): + _inherit = "project.task" -class project_work(osv.osv): - _inherit = "project.task.work" - - def get_user_related_details(self, cr, uid, user_id): + # Compute: effective_hours, total_hours, progress + def _hours_get(self, cr, uid, ids, field_names, args, context=None): res = {} - emp_obj = self.pool.get('hr.employee') - emp_id = emp_obj.search(cr, uid, [('user_id', '=', user_id)]) - if not emp_id: - user_name = self.pool.get('res.users').read(cr, uid, [user_id], ['name'])[0]['name'] - raise UserError(_('Please define employee for user "%s". You must create one.')% (user_name,)) - emp = emp_obj.browse(cr, uid, emp_id[0]) - if not emp.product_id: - raise UserError(_('Please define product and product category property account on the related employee.\nFill in the HR Settings tab of the employee form.')) - - if not emp.journal_id: - raise UserError(_('Please define journal on the related employee.\nFill in the HR Settings tab of the employee form.')) - - acc_id = emp.product_id.property_account_expense.id - if not acc_id: - acc_id = emp.product_id.categ_id.property_account_expense_categ.id - if not acc_id: - raise UserError(_('Please define product and product category property account on the related employee.\nFill in the HR Settings of the employee form.')) - - res['product_id'] = emp.product_id.id - res['journal_id'] = emp.journal_id.id - res['general_account_id'] = acc_id - res['product_uom_id'] = emp.product_id.uom_id.id + tasks_data = self.pool['account.analytic.line'].read_group(cr, uid, [('task_id', 'in', ids)], ['task_id','unit_amount'], ['task_id'], context=context) + for data in tasks_data: + task = self.browse(cr, uid, data['task_id'][0], context=context) + res[data['task_id'][0]] = {'effective_hours': data.get('unit_amount', 0.0), 'remaining_hours': task.planned_hours - data.get('unit_amount', 0.0)} + res[data['task_id'][0]]['total_hours'] = res[data['task_id'][0]]['remaining_hours'] + data.get('unit_amount', 0.0) + res[data['task_id'][0]]['delay_hours'] = res[data['task_id'][0]]['total_hours'] - task.planned_hours + res[data['task_id'][0]]['progress'] = 0.0 + if (task.planned_hours > 0.0 and data.get('unit_amount', 0.0)): + res[data['task_id'][0]]['progress'] = round(min(100.0 * data.get('unit_amount', 0.0) / task.planned_hours, 99.99),2) + # TDE CHECK: if task.state in ('done','cancelled'): + if task.stage_id and task.stage_id.fold: + res[data['task_id'][0]]['progress'] = 100.0 return res - def _create_analytic_entries(self, cr, uid, vals, context): - """Create the hr analytic timesheet from project task work""" - timesheet_obj = self.pool['hr.analytic.timesheet'] - task_obj = self.pool['project.task'] - - vals_line = {} - timeline_id = False - acc_id = False - - task_obj = task_obj.browse(cr, uid, vals['task_id'], context=context) - result = self.get_user_related_details(cr, uid, vals.get('user_id', uid)) - vals_line['name'] = '%s: %s' % (tools.ustr(task_obj.name), tools.ustr(vals['name'] or '/')) - vals_line['user_id'] = vals['user_id'] - vals_line['product_id'] = result['product_id'] - if vals.get('date'): - if len(vals['date']) > 10: - timestamp = datetime.datetime.strptime(vals['date'], tools.DEFAULT_SERVER_DATETIME_FORMAT) - ts = fields.datetime.context_timestamp(cr, uid, timestamp, context) - vals_line['date'] = ts.strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - else: - vals_line['date'] = vals['date'] - - # Calculate quantity based on employee's product's uom - vals_line['unit_amount'] = vals['hours'] - - default_uom = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.project_time_mode_id.id - if result['product_uom_id'] != default_uom: - vals_line['unit_amount'] = self.pool['product.uom']._compute_qty(cr, uid, default_uom, vals['hours'], result['product_uom_id']) - acc_id = task_obj.project_id and task_obj.project_id.analytic_account_id.id or acc_id - if acc_id: - vals_line['account_id'] = acc_id - res = timesheet_obj.on_change_account_id(cr, uid, False, acc_id) - if res.get('value'): - vals_line.update(res['value']) - vals_line['general_account_id'] = result['general_account_id'] - vals_line['journal_id'] = result['journal_id'] - vals_line['amount'] = 0.0 - vals_line['product_uom_id'] = result['product_uom_id'] - amount = vals_line['unit_amount'] - prod_id = vals_line['product_id'] - unit = False - timeline_id = timesheet_obj.create(cr, uid, vals=vals_line, context=context) - - # Compute based on pricetype - amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, timeline_id, - prod_id, amount, False, unit, vals_line['journal_id'], context=context) - if amount_unit and 'amount' in amount_unit.get('value',{}): - updv = { 'amount': amount_unit['value']['amount'] } - timesheet_obj.write(cr, uid, [timeline_id], updv, context=context) - - return timeline_id - - def create(self, cr, uid, vals, *args, **kwargs): - context = kwargs.get('context', {}) - if not context.get('no_analytic_entry',False): - vals['hr_analytic_timesheet_id'] = self._create_analytic_entries(cr, uid, vals, context=context) - return super(project_work,self).create(cr, uid, vals, *args, **kwargs) - - def write(self, cr, uid, ids, vals, context=None): - """ - When a project task work gets updated, handle its hr analytic timesheet. - """ - if context is None: - context = {} - timesheet_obj = self.pool.get('hr.analytic.timesheet') - uom_obj = self.pool.get('product.uom') - result = {} - - if isinstance(ids, (long, int)): - ids = [ids] - - for task in self.browse(cr, uid, ids, context=context): - line_id = task.hr_analytic_timesheet_id - if not line_id: - # if a record is deleted from timesheet, the line_id will become - # null because of the foreign key on-delete=set null - continue - - vals_line = {} - if 'name' in vals: - vals_line['name'] = '%s: %s' % (tools.ustr(task.task_id.name), tools.ustr(vals['name'] or '/')) - if 'user_id' in vals: - vals_line['user_id'] = vals['user_id'] - if 'date' in vals: - vals_line['date'] = vals['date'][:10] - if 'hours' in vals: - vals_line['unit_amount'] = vals['hours'] - prod_id = vals_line.get('product_id', line_id.product_id.id) # False may be set - - # Put user related details in analytic timesheet values - details = self.get_user_related_details(cr, uid, vals.get('user_id', task.user_id.id)) - for field in ('product_id', 'general_account_id', 'journal_id', 'product_uom_id'): - if details.get(field, False): - vals_line[field] = details[field] - - # Check if user's default UOM differs from product's UOM - user_default_uom_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.project_time_mode_id.id - if details.get('product_uom_id', False) and details['product_uom_id'] != user_default_uom_id: - vals_line['unit_amount'] = uom_obj._compute_qty(cr, uid, user_default_uom_id, vals['hours'], details['product_uom_id']) - - # Compute based on pricetype - amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, line_id.id, - prod_id=prod_id, company_id=False, - unit_amount=vals_line['unit_amount'], unit=False, journal_id=vals_line['journal_id'], context=context) - - if amount_unit and 'amount' in amount_unit.get('value',{}): - vals_line['amount'] = amount_unit['value']['amount'] - - if vals_line: - self.pool.get('hr.analytic.timesheet').write(cr, uid, [line_id.id], vals_line, context=context) - - return super(project_work,self).write(cr, uid, ids, vals, context) - - def unlink(self, cr, uid, ids, *args, **kwargs): - hat_obj = self.pool.get('hr.analytic.timesheet') - hat_ids = [] - for task in self.browse(cr, uid, ids): - if task.hr_analytic_timesheet_id: - hat_ids.append(task.hr_analytic_timesheet_id.id) - # Delete entry from timesheet too while deleting entry to task. - if hat_ids: - hat_obj.unlink(cr, uid, hat_ids, *args, **kwargs) - return super(project_work,self).unlink(cr, uid, ids, *args, **kwargs) + def _get_task(self, cr, uid, id, context=None): + res = [] + for line in self.pool.get('account.analytic.line').search_read(cr,uid,[('task_id', '!=', False),('id','in',id)], context=context): + res.append(line['task_id'][0]) + return res - _columns={ - 'hr_analytic_timesheet_id':fields.many2one('hr.analytic.timesheet','Related Timeline Id', ondelete='set null'), + _columns = { + 'remaining_hours': fields.function(_hours_get, string='Remaining Hours', multi='line_id', help="Total remaining time, can be re-estimated periodically by the assignee of the task.", + store = { + 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), + }), + 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='line_id', help="Computed using the sum of the task work done.", + store = { + 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), + }), + 'total_hours': fields.function(_hours_get, string='Total', multi='line_id', help="Computed as: Time Spent + Remaining Time.", + store = { + 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), + }), + 'progress': fields.function(_hours_get, string='Working Time Progress (%)', multi='line_id', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time", + store = { + 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10), + 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), + }), + 'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='line_id', help="Computed as difference between planned hours by the project manager and the total hours of the task.", + store = { + 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), + 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), + }), + 'timesheet_ids': fields.one2many('account.analytic.line', 'task_id', 'Timesheets'), + 'analytic_account_id': fields.related('project_id', 'analytic_account_id', + type='many2one', relation='account.analytic.account', string='Analytic Account', store=True), } + _defaults = { + 'progress': 0, + } -class task(osv.osv): - _inherit = "project.task" - - def unlink(self, cr, uid, ids, *args, **kwargs): - for task_obj in self.browse(cr, uid, ids, *args, **kwargs): - if task_obj.work_ids: - work_ids = [x.id for x in task_obj.work_ids] - self.pool.get('project.task.work').unlink(cr, uid, work_ids, *args, **kwargs) - - return super(task,self).unlink(cr, uid, ids, *args, **kwargs) - - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - task_work_obj = self.pool['project.task.work'] - acc_id = False - missing_analytic_entries = {} - - if vals.get('project_id',False) or vals.get('name',False): - vals_line = {} - hr_anlytic_timesheet = self.pool.get('hr.analytic.timesheet') - if vals.get('project_id',False): - project_obj = self.pool.get('project.project').browse(cr, uid, vals['project_id'], context=context) - acc_id = project_obj.analytic_account_id.id - - for task_obj in self.browse(cr, uid, ids, context=context): - if len(task_obj.work_ids): - for task_work in task_obj.work_ids: - if not task_work.hr_analytic_timesheet_id: - if acc_id : - # missing timesheet activities to generate - missing_analytic_entries[task_work.id] = { - 'name' : task_work.name, - 'user_id' : task_work.user_id.id, - 'date' : task_work.date, - 'account_id': acc_id, - 'hours' : task_work.hours, - 'task_id' : task_obj.id - } - continue - line_id = task_work.hr_analytic_timesheet_id.id - if vals.get('project_id',False): - vals_line['account_id'] = acc_id - if vals.get('name',False): - vals_line['name'] = '%s: %s' % (tools.ustr(vals['name']), tools.ustr(task_work.name) or '/') - hr_anlytic_timesheet.write(cr, uid, [line_id], vals_line, {}) - - res = super(task,self).write(cr, uid, ids, vals, context) - - for task_work_id, analytic_entry in missing_analytic_entries.items(): - timeline_id = task_work_obj._create_analytic_entries(cr, uid, analytic_entry, context=context) - task_work_obj.write(cr, uid, task_work_id, {'hr_analytic_timesheet_id' : timeline_id}, context=context) - - return res + def _prepare_delegate_values(self, cr, uid, ids, delegate_data, context=None): + vals = super(task, self)._prepare_delegate_values(cr, uid, ids, delegate_data, context) + for task in self.browse(cr, uid, ids, context=context): + vals[task.id]['planned_hours'] += task.effective_hours + return vals class res_partner(osv.osv): @@ -295,28 +240,8 @@ class res_partner(osv.osv): return super(res_partner,self).unlink(cursor, user, ids, context=context) - class account_analytic_line(osv.osv): - _inherit = "account.analytic.line" - - def get_product(self, cr, uid, context=None): - emp_obj = self.pool.get('hr.employee') - emp_ids = emp_obj.search(cr, uid, [('user_id', '=', uid)], context=context) - if emp_ids: - employee = emp_obj.browse(cr, uid, emp_ids, context=context)[0] - if employee.product_id:return employee.product_id.id - return False - - _defaults = {'product_id': get_product,} - - def on_change_account_id(self, cr, uid, ids, account_id): - res = {} - if not account_id: - return res - res.setdefault('value',{}) - acc = self.pool.get('account.analytic.account').browse(cr, uid, account_id) - st = acc.to_invoice.id - res['value']['to_invoice'] = st or False - if acc.state == 'close' or acc.state == 'cancelled': - raise UserError(_('You cannot select a Analytic Account which is in Close or Cancelled state.')) - return res + _inherit = "account.analytic.line" + _columns = { + 'task_id' : fields.many2one('project.task', 'Task'), + } diff --git a/addons/project_timesheet/project_timesheet_view.xml b/addons/project_timesheet/project_timesheet_view.xml index e725a4296dd..19624bb5870 100644 --- a/addons/project_timesheet/project_timesheet_view.xml +++ b/addons/project_timesheet/project_timesheet_view.xml @@ -2,6 +2,20 @@ <openerp> <data> + <record id="view_project_tree_inherited" model="ir.ui.view"> + <field name="name">project.project.tree.inherited</field> + <field name="model">project.project</field> + <field name="inherit_id" ref="project.view_project"/> + <field name="arch" type="xml"> + <field name="partner_id" position="after"> + <field name="planned_hours" widget="float_time"/> + <field name="total_hours" widget="float_time"/> + <field name="effective_hours" widget="float_time"/> + <field name="progress_rate" widget="progressbar"/> + </field> + </field> + </record> + <record id="project_invoice_form" model="ir.ui.view"> <field name="name">Inherit project form : Invoicing Data</field> <field name="model">project.project</field> @@ -20,6 +34,12 @@ <field name="analytic_account_id" readonly="1" required="0" class="oe_inline" nolabel="1"/>. </p> </xpath> + <field name="privacy_visibility" position="after"> + <field name="planned_hours" widget="float_time" + groups="project.group_time_work_estimation_tasks"/> + <field name="effective_hours" widget="float_time" + groups="project.group_time_work_estimation_tasks"/> + </field> <xpath expr='//div[@name="buttons"]' position="inside"> <button class="oe_inline oe_stat_button" name="open_timesheets" type="object" attrs="{'invisible':[('invoice_on_timesheets','=',False)]}" icon="fa-calendar" string="Timesheets"/> @@ -40,6 +60,7 @@ </xpath> </field> </record> + <record id="view_account_analytic_line_search_account_inherit" model="ir.ui.view"> <field name="name">account.analytic.line.search.account_id</field> <field name="model">account.analytic.line</field> @@ -48,6 +69,9 @@ <field name="account_id" position="replace"> <field name="account_id" string="Analytic account/project"/> </field> + <field name="account_id" position="after"> + <field name="task_id"/> + </field> </field> </record> <record id="view_account_analytic_line_form_inherit_account_id" model="ir.ui.view"> @@ -56,17 +80,20 @@ <field name="inherit_id" ref="analytic.view_account_analytic_line_form"/> <field name="arch" type="xml"> <field name="account_id" position="replace"> - <field name="account_id" string="Analytic Account/Project" on_change="on_change_account_id(account_id)"/> + <field name="account_id" string="Analytic Account/Project"/> </field> </field> </record> <record id="view_account_analytic_line_tree_inherit_account_id" model="ir.ui.view"> <field name="name">account.analytic.line.tree.account_id</field> <field name="model">account.analytic.line</field> - <field name="inherit_id" ref="analytic.view_account_analytic_line_tree"/> + <field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/> <field name="arch" type="xml"> - <field name="account_id" position="replace"> - <field name="account_id" string="Analytic account/project" on_change="on_change_account_id(account_id)"/> + <field name="account_id" position="attributes"> + <attribute name="string">Analytic account/project</attribute> + </field> + <field name="account_id" position="after"> + <field name="task_id" domain="[('project_id.analytic_account_id', '=' , account_id)]"/> </field> </field> </record> @@ -118,5 +145,65 @@ <menuitem id="menu_invoicing_contracts" parent="menu_project_billing" sequence="4" action="account_analytic_analysis.action_account_analytic_overdue"/> + <record model="ir.ui.view" id="view_task_form2_inherited"> + <field name="name">project.task.form.inherited</field> + <field name="model">project.task</field> + <field name="inherit_id" ref="project.view_task_form2" /> + <field name="arch" type="xml"> + <field name="categ_ids" position="after"> + <field name="analytic_account_id" invisible="1"/> + <field name="progress" widget="progressbar" + groups="project.group_time_work_estimation_tasks"/> + </field> + <xpath expr="//notebook/page[@name='description_page']" position="after"> + <page string="Timesheets"> + <field name="timesheet_ids" groups="project.group_tasks_work_on_tasks" context="{'default_account_id' : analytic_account_id, 'default_is_timesheet' : 1}"> + <tree editable="top" string="Timesheet Activities"> + <field name="date"/> + <field name="user_id" required="1"/> + <field name="name"/> + <field name="account_id" on_change="on_change_account_id(account_id, user_id, unit_amount, is_timesheet, context)"/> + <field name="unit_amount" string="Duration" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id, context)" sum="Total time" widget="float_time"/> + <field name="to_invoice"/> + <field name="product_uom_id" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id, context)" invisible="1"/> + <field name="journal_id" invisible="1"/> + <field name="amount" sum="Total cost" invisible="1"/> + <field name="general_account_id" invisible="1"/> + <field name="product_id" on_change="on_change_unit_amount(product_id, unit_amount, False, product_uom_id,journal_id, context)" required="1" domain="[('type','=','service')]" invisible="1"/> + <field name="is_timesheet" invisible="1"/> + </tree> + </field> + <group> + <group class="oe_subtotal_footer oe_right" name="project_hours" groups="project.group_time_work_estimation_tasks"> + <field name="effective_hours" widget="float_time" groups="project.group_time_work_estimation_tasks"/> + <field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" groups="project.group_time_work_estimation_tasks"/> + </group> + </group> + </page> + </xpath> + <xpath expr="//tree[@name='delegated_task']" position="inside"> + <field name="effective_hours" widget="float_time"/> + <field name="progress" widget="progressbar"/> + <field name="remaining_hours" widget="float_time"/> + </xpath> + </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="project.view_task_tree2" /> + <field eval="2" name="priority"/> + <field name="arch" type="xml"> + <field name="planned_hours" position="after"> + <field name="remaining_hours" widget="float_time" sum="Remaining Hours" on_change="onchange_remaining(remaining_hours,planned_hours)" invisible="context.get('set_visible',False)" groups="project.group_time_work_estimation_tasks"/> + <field name="total_hours" invisible="1"/> + <field name="effective_hours" widget="float_time" sum="Spent Hours" invisible="1"/> + </field> + <field name="date_end" position="after"> + <field name="progress" widget="progressbar" invisible="context.get('set_visible',False)"/> + </field> + </field> + </record> </data> </openerp> diff --git a/addons/project_timesheet/report/__init__.py b/addons/project_timesheet/report/__init__.py new file mode 100644 index 00000000000..334f01258a0 --- /dev/null +++ b/addons/project_timesheet/report/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import project_report
\ No newline at end of file diff --git a/addons/project_timesheet/report/project_report.py b/addons/project_timesheet/report/project_report.py new file mode 100644 index 00000000000..f3cee3e1cfd --- /dev/null +++ b/addons/project_timesheet/report/project_report.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from openerp.osv import fields,osv +from openerp import tools + +class report_project_task_user(osv.Model): + _inherit = "report.project.task.user" + _columns = { + 'hours_planned': fields.float('Planned Hours', readonly=True), + 'hours_effective': fields.float('Effective Hours', readonly=True), + 'hours_delay': fields.float('Avg. Plan.-Eff.', readonly=True), + 'remaining_hours': fields.float('Remaining Hours', readonly=True), + 'progress': fields.float('Progress', readonly=True, group_operator='avg'), + 'total_hours': fields.float('Total Hours', readonly=True), + } + + def _select(self): + return super(report_project_task_user, self)._select() + ", progress as progress, t.effective_hours as hours_effective, remaining_hours as remaining_hours, total_hours as total_hours, t.delay_hours as hours_delay, planned_hours as hours_planned" + + def _group_by(self): + return super(report_project_task_user, self)._group_by() + ", remaining_hours, t.effective_hours, progress, total_hours, planned_hours, hours_delay" + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file diff --git a/addons/project_timesheet/report/project_report_view.xml b/addons/project_timesheet/report/project_report_view.xml new file mode 100644 index 00000000000..35287e1f14f --- /dev/null +++ b/addons/project_timesheet/report/project_report_view.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + <record id="view_task_project_user_graph_inherited" model="ir.ui.view"> + <field name="name">report.project.task.user.graph.inherited</field> + <field name="model">report.project.task.user</field> + <field name="inherit_id" ref="project.view_task_project_user_graph" /> + <field name="arch" type="xml"> + <graph string="Tasks Analysis" type="pivot"> + <field name="project_id" position="after"> + <field name="total_hours" type="measure"/> + <field name="hours_planned" type="measure"/> + <field name="remaining_hours" type="measure"/> + </field> + </graph> + </field> + </record> + </data> +</openerp>
\ No newline at end of file diff --git a/addons/project_timesheet/security/ir.model.access.csv b/addons/project_timesheet/security/ir.model.access.csv index e8f7badf40a..ffd4aa2094e 100644 --- a/addons/project_timesheet/security/ir.model.access.csv +++ b/addons/project_timesheet/security/ir.model.access.csv @@ -1,4 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_hr_analytic_timesheet_user,hr.analytic.timesheet.user,hr_timesheet.model_hr_analytic_timesheet,project.group_project_user,1,1,1,0
access_sale_order_manager,sale.order,sale.model_sale_order,project.group_project_manager,1,0,0,0
access_sale_order_user,sale.order,sale.model_sale_order,project.group_project_user,1,0,0,0
diff --git a/addons/project_timesheet/test/work_timesheet.yml b/addons/project_timesheet/test/work_timesheet.yml deleted file mode 100644 index 9c28ec3a76f..00000000000 --- a/addons/project_timesheet/test/work_timesheet.yml +++ /dev/null @@ -1,22 +0,0 @@ -- - I set task work entry for Employee -- - !record {model: project.task.work, id: project_task_work1}: - date: !eval time.strftime('%Y-%m-%d %H:%M:%S') - name: Test Timesheet records - task_id: project.project_task_10 - hours: 20.0 - user_id: base.user_demo - company_id: base.main_company -- - I check Timesheet line for employee in current Timesheet -- - !python {model: hr.analytic.timesheet}: | - from openerp.osv import fields - start = fields.date.context_today(self, cr, uid, context=context) - task_work = self.search(cr, uid, [("name","=","Social network integration: Test Timesheet records")],context)[0] - task_ids = self.browse(cr, uid, task_work, context) - assert task_ids.user_id.id == ref("base.user_demo"), 'Error, The User in Timesheet is not Correct' - assert task_ids.date == start, 'Error, The Date in Timesheet is not ok' - assert task_ids.product_uom_id.name == "Hour(s)", 'Error, The Hour in Timesheet is not ok' - diff --git a/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml b/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml index c62c5584516..d1dc5a9214a 100644 --- a/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml +++ b/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml @@ -31,6 +31,19 @@ name: HR Timesheet type: general - + Create a user type +- + !record {model: account.account.type, id: user_type0}: + name: Other + code: 990099 +- + Create a financial account +- + !record {model: account.account, id: financial_account0}: + name: expenses + code: 9999999 + user_type: user_type0 +- Create an employee 'HR Manager' for user 'HR Manager' - !record {model: hr.employee, id: hr_employee_hrmanager0}: @@ -58,8 +71,11 @@ Make a work task entry 'Get work calendar of all employees' of 10 hours done by HR manager - !record {model: project.task, id: project_task_getalltimesheetrecords0}: - work_ids: + timesheet_ids: - date: !eval time.strftime('%Y-05-%d %H:%M:%S') - hours: 10.0 + unit_amount: 10.0 name: Get work calendar of all employees user_id: res_users_hrmanager0 + account_id: analytic.analytic_administratif + journal_id: account_analytic_journal_hrtimesheet0 + general_account_id: financial_account0 |
