diff --git a/addons/account/account.py b/addons/account/account.py index c9dd1950347..cfba30856a9 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -541,10 +541,18 @@ class account_account(osv.osv): return False return True + def _check_company_account(self, cr, uid, ids, context=None): + for account in self.browse(cr, uid, ids, context=context): + if account.parent_id: + if account.company_id != account.parent_id.company_id: + return False + return True + _constraints = [ (_check_recursion, 'Error!\nYou cannot create recursive accounts.', ['parent_id']), (_check_type, 'Configuration Error!\nYou cannot define children to an account with internal type different of "View".', ['type']), (_check_account_type, 'Configuration Error!\nYou cannot select an account type with a deferral method different of "Unreconciled" for accounts with internal type "Payable/Receivable".', ['user_type','type']), + (_check_company_account, 'Error!\nYou cannot create an account which has parent account of different company.', ['parent_id']), ] _sql_constraints = [ ('code_company_uniq', 'unique (code,company_id)', 'The code of the account must be unique per company !') diff --git a/addons/account/account_analytic_line.py b/addons/account/account_analytic_line.py index f0a420f7496..066f8d1abec 100644 --- a/addons/account/account_analytic_line.py +++ b/addons/account/account_analytic_line.py @@ -107,7 +107,7 @@ class account_analytic_line(osv.osv): if journal_id: journal = analytic_journal_obj.browse(cr, uid, journal_id, context=context) if journal.type == 'sale': - product_price_type_ids = product_price_type_obj.search(cr, uid, [('field','=','list_price')], context) + product_price_type_ids = product_price_type_obj.search(cr, uid, [('field','=','list_price')], context=context) if product_price_type_ids: pricetype = product_price_type_obj.browse(cr, uid, product_price_type_ids, context=context)[0] # Take the company currency as the reference one diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index 8e79376aa87..4422537007f 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -486,6 +486,19 @@ class account_bank_statement(osv.osv): default['move_line_ids'] = [] return super(account_bank_statement, self).copy(cr, uid, id, default, context=context) + def button_journal_entries(self, cr, uid, ids, context=None): + ctx = (context or {}).copy() + ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id + return { + 'view_type':'form', + 'view_mode':'tree', + 'res_model':'account.move.line', + 'view_id':False, + 'type':'ir.actions.act_window', + 'domain':[('statement_id','in',ids)], + 'context':ctx, + } + account_bank_statement() class account_bank_statement_line(osv.osv): diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index b01fc974bef..3e86c4963b7 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -402,6 +402,7 @@ class account_invoice(osv.osv): 'default_res_id': ids[0], 'default_use_template': True, 'default_template_id': template_id, + 'default_composition_mode': 'comment', }) return { 'view_type': 'form', @@ -431,7 +432,7 @@ class account_invoice(osv.osv): if t['state'] in ('draft', 'cancel') and t['internal_number']== False: unlink_ids.append(t['id']) else: - raise osv.except_osv(_('Invalid Action!'), _('You cannot delete an invoice which is open or paid. You should refund it instead.')) + raise osv.except_osv(_('Invalid Action!'), _('You can not delete an invoice which is not cancelled. You should refund it instead.')) osv.osv.unlink(self, cr, uid, unlink_ids, context=context) return True @@ -983,13 +984,13 @@ class account_invoice(osv.osv): for i in line: i[2]['period_id'] = period_id + ctx.update(invoice=inv) move_id = move_obj.create(cr, uid, move, context=ctx) new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name # make the invoice point to that move self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx) # Pass invoice in context in method post: used if you want to get the same # account move reference when creating the same invoice after a cancelled one: - ctx.update({'invoice':inv}) move_obj.post(cr, uid, [move_id], context=ctx) self._log_event(cr, uid, ids) return True diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index 2f722d07aeb..d3697b13ea9 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -168,10 +168,8 @@ context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}" domain="[('supplier', '=', True)]"/> - - - - + + + + + fleet.vehicle.tree + fleet.vehicle + + + + + + + + + + + + + + + + + + + fleet.vehicle.search + fleet.vehicle + + + + + + + + + + + + + + + + fleet.vehicle.kanban + fleet.vehicle + + + + + + + + + + + + + + + + + +
+ + + +
+

+ +
+ +
+

+ + + + and other(s) + + + + + + + + and other(s) + + + + +
    +
  • + +
  • +
  • + +
  • + +
+ +
+ +
+ +
+ +
+
+ + + + + + + + + Vehicles + fleet.vehicle + form + kanban,tree,form + +

+ Click to create a new vehicle. +

+ You will be able to manage your fleet by keeping track of the + contracts, services, fixed and recurring costs, odometers and + fuel logs associated to each vehicle. +

+ OpenERP will warn you when services or contract have to be + renewed. +

+
+
+ + + + + + + + fleet.vehicle.log_contract.form + fleet.vehicle.log.contract + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+ + + +
+
+
+
+ + + + + + fleet.vehicle.log.contract.tree + fleet.vehicle.log.contract + + + + + + + + + + + + + + + + + + fleet.vehicle.log.contract.graph + fleet.vehicle.log.contract + + + + + + + + + + + + Vehicles Contracts + fleet.vehicle.log.contract + form + tree,form,graph + +

+ Click to create a new contract. +

+ Manage all your contracts (leasing, insurances, etc.) with + their related services, costs. OpenERP will automatically warn + you when some contracts have to be renewed. +

+ Each contract (e.g.: leasing) may include several services + (reparation, insurances, periodic maintenance). +

+
+
+ + + + + fleet.vehicle.odometer.form + fleet.vehicle.odometer + +
+ + + +
+ + +
+ +
+
+
+
+
+ + + fleet.vehicle.odometer.tree + fleet.vehicle.odometer + + + + + + + + + + + + + fleet.vehicle.odometer.graph + fleet.vehicle.odometer + + + + + + + + + + + + Vehicles Odometer + fleet.vehicle.odometer + form + tree,graph + {"search_default_groupby_vehicle" : True} + +

+ Here you can add various odometer entries for all vehicles. + You can also show odometer value for a particular vehicle using + the search field. +

+
+
+ + + + + fleet.vehicle.log.fuel.form + fleet.vehicle.log.fuel + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + fleet.vehicle.log.fuel.tree + fleet.vehicle.log.fuel + + + + + + + + + + + + + + + + + + fleet.vehicle.log.fuel.graph + fleet.vehicle.log.fuel + + + + + + + + + + + + Vehicles Fuel Logs + fleet.vehicle.log.fuel + form + tree,form,graph + +

+ Click to create a new fuel log. +

+ Here you can add refuelling entries for all vehicles. You can + also filter logs of a particular vehicle using the search + field. +

+
+
+ + + + + + fleet.vehicle.log.services.form + fleet.vehicle.log.services + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + fleet.vehicle.log.services.tree + fleet.vehicle.log.services + + + + + + + + + + + + + + + + fleet.vehicle.log.services.graph + fleet.vehicle.log.services + + + + + + + + + + + Vehicles Services Logs + fleet.vehicle.log.services + form + tree,form,graph + +

+ Click to create a new service entry. +

+ OpenERP helps you keeping track of all the services done + on your vehicle. Services can be of many type: occasional + repair, fixed maintenance, etc. +

+
+
+ + + + + fleet.service.type.tree + fleet.service.type + + + + + + + + + + Service Types + fleet.service.type + form + tree,form + +

+ Click to create a new type of service. +

+ Each service can used in contracts, as a standalone service or both. +

+
+
+ + + + + + fleet.vehicle.cost.tree + fleet.vehicle.cost + + + + + + + + + + + + + + + fleet.vehicle.cost.search + fleet.vehicle.cost + + + + + + + + + + + + + + + + + + + + + + + fleet.vehicle.cost.form + fleet.vehicle.cost + +
+ + + + + + + + + + + + + +
+
+
+ + + fleet.vehicle.cost.graph + fleet.vehicle.cost + + + + + + + + + + + Vehicle Costs + fleet.vehicle.cost + form + tree,form,graph + {"search_default_parent_false" : True, "search_default_groupby_vehicle_id" : True,} + +

+ Click to create a new cost. +

+ OpenERP helps you managing the costs for your different + vehicles. Costs are created automatically from services, + contracts (fixed or recurring) and fuel logs. +

+
+
+ + + + + diff --git a/addons/fleet/security/ir.model.access.csv b/addons/fleet/security/ir.model.access.csv new file mode 100644 index 00000000000..823b3cb2945 --- /dev/null +++ b/addons/fleet/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +fleet_vehicle_model_access_right,fleet_vehicle_model_access_right,model_fleet_vehicle_model,,1,1,1,1 +fleet_vehicle_tag_access_right,fleet_vehicle_tag_access_right,model_fleet_vehicle_tag,,1,1,1,1 +fleet_vehicle_state_access_right,fleet_vehicle_state_access_right,model_fleet_vehicle_state,,1,1,1,1 +fleet_vehicle_odometer_access_right,fleet_vehicle_odometer_access_right,model_fleet_vehicle_odometer,,1,1,1,1 +fleet_vehicle_model_brand_access_right,fleet_vehicle_model_brand_access_right,model_fleet_vehicle_model_brand,,1,1,1,1 +fleet_vehicle_access_right,fleet_vehicle_access_right,model_fleet_vehicle,,1,1,1,1 +fleet_vehicle_log_fuel_access_right,fleet_vehicle_log_fuel_access_right,model_fleet_vehicle_log_fuel,,1,1,1,1 +fleet_vehicle_log_services_access_right,fleet_vehicle_log_services_access_right,model_fleet_vehicle_log_services,,1,1,1,1 +fleet_vehicle_log_contract_access_right,fleet_vehicle_log_contract_access_right,model_fleet_vehicle_log_contract,,1,1,1,1 +fleet_service_type_access_right,fleet_service_type_access_right,model_fleet_service_type,,1,1,1,1 +access_fleet_vehicle_cost,access_fleet_vehicle_cost,model_fleet_vehicle_cost,,1,1,1,1 +access_fleet_contract_state,access_fleet_contract_state,model_fleet_contract_state,,1,1,1,1 diff --git a/addons/fleet/static/src/img/icon.png b/addons/fleet/static/src/img/icon.png new file mode 100644 index 00000000000..5b41e43c5bd Binary files /dev/null and b/addons/fleet/static/src/img/icon.png differ diff --git a/addons/google_base_account/i18n/es.po b/addons/google_base_account/i18n/es.po new file mode 100644 index 00000000000..c0779f7159a --- /dev/null +++ b/addons/google_base_account/i18n/es.po @@ -0,0 +1,124 @@ +# Spanish translation for openobject-addons +# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-02-08 00:36+0000\n" +"PO-Revision-Date: 2012-11-07 12:33+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Spanish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2012-11-08 04:47+0000\n" +"X-Generator: Launchpad (build 16232)\n" + +#. module: google_base_account +#: field:res.users,gmail_user:0 +msgid "Username" +msgstr "Nombre de usuario" + +#. module: google_base_account +#: model:ir.actions.act_window,name:google_base_account.act_google_login_form +msgid "Google Login" +msgstr "Acceso de Google" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:29 +#, python-format +msgid "Google Contacts Import Error!" +msgstr "¡Error en la importacion de los contactos de Google!" + +#. module: google_base_account +#: view:res.users:0 +msgid " Synchronization " +msgstr " Sincronización " + +#. module: google_base_account +#: sql_constraint:res.users:0 +msgid "You can not have two users with the same login !" +msgstr "¡No puede tener dos usuarios con el mismo identificador!" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:75 +#, python-format +msgid "Error" +msgstr "Error" + +#. module: google_base_account +#: view:google.login:0 +msgid "Google login" +msgstr "Acceso Google" + +#. module: google_base_account +#: model:ir.model,name:google_base_account.model_res_users +msgid "res.users" +msgstr "Usuarios" + +#. module: google_base_account +#: field:google.login,password:0 +msgid "Google Password" +msgstr "Contraseña de Google" + +#. module: google_base_account +#: view:google.login:0 +msgid "_Cancel" +msgstr "_Cancelar" + +#. module: google_base_account +#: view:res.users:0 +msgid "Google Account" +msgstr "Cuenta de Google" + +#. module: google_base_account +#: field:google.login,user:0 +msgid "Google Username" +msgstr "Usuario de Google" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:29 +#, python-format +msgid "" +"Please install gdata-python-client from http://code.google.com/p/gdata-" +"python-client/downloads/list" +msgstr "" +"Por favor, instale gdata-python-client de http://code.google.com/p/gdata-" +"python-client/downloads/list o desde el administrador de paquetes de su " +"distribución" + +#. module: google_base_account +#: model:ir.model,name:google_base_account.model_google_login +msgid "Google Contact" +msgstr "Contacto Google" + +#. module: google_base_account +#: view:google.login:0 +msgid "_Login" +msgstr "Iniciar _sesión" + +#. module: google_base_account +#: constraint:res.users:0 +msgid "The chosen company is not in the allowed companies for this user" +msgstr "" +"La compañía seleccionada no está entre las companías permitidas para este " +"usuario" + +#. module: google_base_account +#: field:res.users,gmail_password:0 +msgid "Password" +msgstr "Contraseña" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:75 +#, python-format +msgid "Authentication fail check the user and password !" +msgstr "Fallo en la autenticación. ¡Compruebe el usuario y la contraseña!" + +#. module: google_base_account +#: view:google.login:0 +msgid "ex: user@gmail.com" +msgstr "ejemplo: usuario@gmail.com" diff --git a/addons/google_base_account/i18n/mn.po b/addons/google_base_account/i18n/mn.po new file mode 100644 index 00000000000..940a83c37d7 --- /dev/null +++ b/addons/google_base_account/i18n/mn.po @@ -0,0 +1,119 @@ +# Mongolian translation for openobject-addons +# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-02-08 00:36+0000\n" +"PO-Revision-Date: 2012-11-08 03:11+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Mongolian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2012-11-09 04:39+0000\n" +"X-Generator: Launchpad (build 16250)\n" + +#. module: google_base_account +#: field:res.users,gmail_user:0 +msgid "Username" +msgstr "Хэрэглэгчийн нэр" + +#. module: google_base_account +#: model:ir.actions.act_window,name:google_base_account.act_google_login_form +msgid "Google Login" +msgstr "" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:29 +#, python-format +msgid "Google Contacts Import Error!" +msgstr "" + +#. module: google_base_account +#: view:res.users:0 +msgid " Synchronization " +msgstr "" + +#. module: google_base_account +#: sql_constraint:res.users:0 +msgid "You can not have two users with the same login !" +msgstr "Ижил нэвтрэх нэртэй хоёр хэрэглэгч байж болохгүй!" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:75 +#, python-format +msgid "Error" +msgstr "Алдаа" + +#. module: google_base_account +#: view:google.login:0 +msgid "Google login" +msgstr "" + +#. module: google_base_account +#: model:ir.model,name:google_base_account.model_res_users +msgid "res.users" +msgstr "res.users" + +#. module: google_base_account +#: field:google.login,password:0 +msgid "Google Password" +msgstr "" + +#. module: google_base_account +#: view:google.login:0 +msgid "_Cancel" +msgstr "_Цуцлах" + +#. module: google_base_account +#: view:res.users:0 +msgid "Google Account" +msgstr "" + +#. module: google_base_account +#: field:google.login,user:0 +msgid "Google Username" +msgstr "" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:29 +#, python-format +msgid "" +"Please install gdata-python-client from http://code.google.com/p/gdata-" +"python-client/downloads/list" +msgstr "" + +#. module: google_base_account +#: model:ir.model,name:google_base_account.model_google_login +msgid "Google Contact" +msgstr "" + +#. module: google_base_account +#: view:google.login:0 +msgid "_Login" +msgstr "" + +#. module: google_base_account +#: constraint:res.users:0 +msgid "The chosen company is not in the allowed companies for this user" +msgstr "" + +#. module: google_base_account +#: field:res.users,gmail_password:0 +msgid "Password" +msgstr "" + +#. module: google_base_account +#: code:addons/google_base_account/wizard/google_login.py:75 +#, python-format +msgid "Authentication fail check the user and password !" +msgstr "" + +#. module: google_base_account +#: view:google.login:0 +msgid "ex: user@gmail.com" +msgstr "" diff --git a/addons/google_docs/static/src/js/gdocs.js b/addons/google_docs/static/src/js/gdocs.js index 0cc93b2510c..45e1d09d42c 100644 --- a/addons/google_docs/static/src/js/gdocs.js +++ b/addons/google_docs/static/src/js/gdocs.js @@ -16,9 +16,9 @@ var _t = instance.web._t, var view = self.getParent(); var ids = ( view.fields_view.type != "form" )? view.groups.get_selection().ids : [ view.datarecord.id ]; if( !_.isEmpty(ids) ){ - view.sidebar_context().then(function (context) { + view.sidebar_context().done(function (context) { var ds = new instance.web.DataSet(this, 'ir.attachment', context); - ds.call('google_doc_get', [view.dataset.model, ids, context]).then(function(r) { + ds.call('google_doc_get', [view.dataset.model, ids, context]).done(function(r) { if (r == 'False') { var params = { error: response, diff --git a/addons/hr/__openerp__.py b/addons/hr/__openerp__.py index cf3a7936657..b4b8e982845 100644 --- a/addons/hr/__openerp__.py +++ b/addons/hr/__openerp__.py @@ -24,7 +24,7 @@ 'version': '1.1', 'author': 'OpenERP SA', 'category': 'Human Resources', - 'sequence': 12, + 'sequence': 21, 'website': 'http://www.openerp.com', 'summary': 'Jobs, Departments, Employees Details', 'description': """ diff --git a/addons/hr/hr.py b/addons/hr/hr.py index d57fcf49ab2..5f3a5653307 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -158,6 +158,8 @@ class hr_employee(osv.osv): return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) _columns = { + #we need a related field in order to be able to sort the employee by name + 'name_related': fields.related('resource_id', 'name', type='char', string='Name', readonly=True, store=True), 'country_id': fields.many2one('res.country', 'Nationality'), 'birthday': fields.date("Date of Birth"), 'ssnid': fields.char('SSN No', size=32, help='Social Security Number'), @@ -207,6 +209,8 @@ class hr_employee(osv.osv): 'last_login': fields.related('user_id', 'date', type='datetime', string='Latest Connection', readonly=1), } + _order='name_related' + def create(self, cr, uid, data, context=None): employee_id = super(hr_employee, self).create(cr, uid, data, context=context) try: diff --git a/addons/hr/hr_data.xml b/addons/hr/hr_data.xml index 514cf1f80f6..25c882f2201 100644 --- a/addons/hr/hr_data.xml +++ b/addons/hr/hr_data.xml @@ -6,11 +6,11 @@ mail.group notification + Employee Directory application installed! - Manage your human resources with OpenERP: employees and their hierarchy, HR departments and job positions. - -More HR features are available via extra applications: Recruitment Process (manage job positions and recruitment), Timesheet Validation (record timesheets and attendance), -Leave Management (keep track of employee leaves), Expense Management (manage employee expenses), Employee Appraisals (organize employee surveys, where employees evaluate their subordinates or their manager). + Manage your human resources with OpenERP: employees and their hierarchy, HR departments and job positions.

+

More HR features are available via extra applications: Recruitment Process (manage job positions and recruitment), Timesheet Validation (record timesheets and attendance), +Leave Management (keep track of employee leaves), Expense Management (manage employee expenses), Employee Appraisals (organize employee surveys, where employees evaluate their subordinates or their manager).

]]>
diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index c4a622d7a4f..8918c5012c3 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -288,7 +288,7 @@ - Categories of Employee + Categories of Employees hr.employee.category form tree,form @@ -342,8 +342,8 @@ - + diff --git a/addons/hr/i18n/es_EC.po b/addons/hr/i18n/es_EC.po index 32e81b6d14d..bfbbb7fa180 100644 --- a/addons/hr/i18n/es_EC.po +++ b/addons/hr/i18n/es_EC.po @@ -8,15 +8,14 @@ msgstr "" "Project-Id-Version: openobject-addons\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2012-02-08 01:37+0100\n" -"PO-Revision-Date: 2012-01-03 02:55+0000\n" -"Last-Translator: Christopher Ormaza - (Ecuadorenlinea.net) " -"\n" +"PO-Revision-Date: 2012-11-10 17:20+0000\n" +"Last-Translator: Cristian Salamea (Gnuthink) \n" "Language-Team: Spanish (Ecuador) \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2012-10-30 05:17+0000\n" -"X-Generator: Launchpad (build 16206)\n" +"X-Launchpad-Export-Date: 2012-11-11 04:57+0000\n" +"X-Generator: Launchpad (build 16251)\n" #. module: hr #: model:process.node,name:hr.process_node_openerpuser0 @@ -199,6 +198,8 @@ msgstr "Mujer" msgid "" "Expected number of employees for this job position after new recruitment." msgstr "" +"Número de Empleados para este puesto de trabajo después de las nuevas " +"contrataciones" #. module: hr #: model:ir.ui.menu,name:hr.menu_open_view_attendance_reason_new_config @@ -291,7 +292,7 @@ msgstr "Categorías" #. module: hr #: field:hr.job,expected_employees:0 msgid "Total Employees" -msgstr "" +msgstr "Total de Empleados" #. module: hr #: selection:hr.employee,marital:0 @@ -435,7 +436,7 @@ msgstr "Estado" #: model:ir.actions.act_window,name:hr.open_view_categ_tree #: model:ir.ui.menu,name:hr.menu_view_employee_category_tree msgid "Categories Structure" -msgstr "" +msgstr "Estructura de categorías" #. module: hr #: field:hr.employee,partner_id:0 @@ -465,7 +466,7 @@ msgstr "¡Error! No se puede crear una jerarquía recursiva de empleados." #. module: hr #: model:ir.actions.act_window,name:hr.action2 msgid "Subordinate Hierarchy" -msgstr "" +msgstr "Jerarquía subirdinada" #. module: hr #: model:ir.actions.act_window,help:hr.view_department_form_installer @@ -715,12 +716,12 @@ msgstr "Subordinados" #. module: hr #: field:hr.job,no_of_employee:0 msgid "Number of employees currently occupying this job position." -msgstr "" +msgstr "Número de empleados ocupando actualmente este puesto de trabajo" #. module: hr #: field:hr.job,no_of_recruitment:0 msgid "Number of new employees you expect to recruit." -msgstr "" +msgstr "Número de empleados que espera contratar" #~ msgid "Working Time Categories" #~ msgstr "Categorías de Horarios de Trabajo" diff --git a/addons/hr/static/src/css/hr.css b/addons/hr/static/src/css/hr.css index 00ef92c0fd8..f395f9939e5 100644 --- a/addons/hr/static/src/css/hr.css +++ b/addons/hr/static/src/css/hr.css @@ -18,10 +18,11 @@ -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px; + border-collapse: separate; -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); -o-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); - -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + box-shadow: 0 1px 4px 3px rgba(0, 0, 0, 0.4); } .oe_employee_picture { diff --git a/addons/hr_attendance/report/attendance_by_month.py b/addons/hr_attendance/report/attendance_by_month.py index f104b9c287a..6d41869ff23 100644 --- a/addons/hr_attendance/report/attendance_by_month.py +++ b/addons/hr_attendance/report/attendance_by_month.py @@ -31,6 +31,7 @@ from report.interface import toxml from report import report_sxw from tools import ustr from tools.translate import _ +from tools import to_xml one_day = relativedelta(days=1) month2name = [0, 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] @@ -111,7 +112,7 @@ class report_custom(report_rml): %s %s - ''' % (str(rml_obj.formatLang(time.strftime("%Y-%m-%d"),date=True))+' ' + str(time.strftime("%H:%M")),pooler.get_pool(cr.dbname).get('res.users').browse(cr,uid,uid).company_id.name) + ''' % (str(rml_obj.formatLang(time.strftime("%Y-%m-%d"),date=True))+' ' + str(time.strftime("%H:%M")),to_xml(pooler.get_pool(cr.dbname).get('res.users').browse(cr,uid,uid).company_id.name)) first_date = str(month) som = datetime.strptime(first_date, '%Y-%m-%d %H:%M:%S') diff --git a/addons/hr_attendance/static/src/js/attendance.js b/addons/hr_attendance/static/src/js/attendance.js index d92e7534786..84516353b9d 100644 --- a/addons/hr_attendance/static/src/js/attendance.js +++ b/addons/hr_attendance/static/src/js/attendance.js @@ -57,7 +57,7 @@ openerp.hr_attendance = function (instance) { var employee = new instance.web.DataSetSearch(self, 'hr.employee', self.session.user_context, [ ['user_id', '=', self.session.uid] ]); - return employee.read_slice(['id', 'name', 'state', 'last_sign', 'attendance_access']).pipe(function (res) { + return employee.read_slice(['id', 'name', 'state', 'last_sign', 'attendance_access']).then(function (res) { if (_.isEmpty(res) ) return; if (res[0].attendance_access == false){ @@ -75,7 +75,7 @@ openerp.hr_attendance = function (instance) { do_update: function () { this._super(); var self = this; - this.update_promise = this.update_promise.then(function () { + this.update_promise = this.update_promise.done(function () { if (self.attendanceslider) return; self.attendanceslider = new instance.hr_attendance.AttendanceSlider(self); diff --git a/addons/hr_evaluation/__openerp__.py b/addons/hr_evaluation/__openerp__.py index fb3d87f2bc1..fb303ff28e4 100644 --- a/addons/hr_evaluation/__openerp__.py +++ b/addons/hr_evaluation/__openerp__.py @@ -24,6 +24,7 @@ 'version': '0.1', 'author': 'OpenERP SA', 'category': 'Human Resources', + 'sequence': 31, 'website': 'http://www.openerp.com', 'summary': 'Periodical Evaluations, Appraisals, Surveys', 'images': ['images/hr_evaluation_analysis.jpeg','images/hr_evaluation.jpeg'], diff --git a/addons/hr_evaluation/hr_evaluation.py b/addons/hr_evaluation/hr_evaluation.py index b13e90cd627..eece0c7497e 100644 --- a/addons/hr_evaluation/hr_evaluation.py +++ b/addons/hr_evaluation/hr_evaluation.py @@ -291,6 +291,7 @@ survey_request() class hr_evaluation_interview(osv.osv): _name = 'hr.evaluation.interview' _inherits = {'survey.request': 'request_id'} + _inherit = 'mail.thread' _rec_name = 'request_id' _description = 'Appraisal Interview' _columns = { diff --git a/addons/hr_evaluation/hr_evaluation_data.xml b/addons/hr_evaluation/hr_evaluation_data.xml index 01915de76a0..2afb5200091 100644 --- a/addons/hr_evaluation/hr_evaluation_data.xml +++ b/addons/hr_evaluation/hr_evaluation_data.xml @@ -11,9 +11,9 @@ mail.group notification + Employee Appraisals application installed! - Manage employee reviews: you can define an appraisal campaign with several steps, with specific evaluation surveys according to hierarchy levels. -Evaluations filled by employees may be exported as pdf files. + Manage employee reviews: you can define an appraisal campaign with several steps, with specific evaluation surveys according to hierarchy levels. Evaluations filled by employees may be exported as pdf files.

]]>
@@ -811,8 +811,8 @@ Once the form had been filled, the employee send it to his supervisor. The comment you entered is in an invalid format. * His direct reports will be invited through OpenERP to express a feedback on their supervisor's leadership and to give their opinion about their own engagement and effectiveness, the continuous improvement and openness in action in the company, ... -* The employees will send back their anonymous answers to OpenERP. The data will be handled by the HR manager and a brief summary of the data will be sent to the concerned supervisor, to his team and to the supervisor's supervisor. -* The appraiser should rate the employee’s major work accomplishments and performance according to the metric provided below : +* The employees will send back their anonymous answers to OpenERP. The data will be handled by the HR manager and a brief summary of the data will be sent to the concerned supervisor, to his team and to the supervisor's supervisor. +* The appraiser should rate the employee’s major work accomplishments and performance according to the metric provided below : 1 - Significantly exceeds standards and expectations required of the position 2 - Exceeds standards and expectations diff --git a/addons/hr_evaluation/hr_evaluation_view.xml b/addons/hr_evaluation/hr_evaluation_view.xml index 697ed6bacea..e0978847acb 100644 --- a/addons/hr_evaluation/hr_evaluation_view.xml +++ b/addons/hr_evaluation/hr_evaluation_view.xml @@ -266,7 +266,7 @@ Each employee may be assigned an Appraisal Plan. Such a plan defines the frequency and the way you manage your periodic personnel evaluation. You will be able to define steps and - attach interviews to each step. OpenERP manages all kind of + attach interviews to each step. OpenERP manages all kinds of evaluations: bottom-up, top-down, self-evaluation and final evaluation by the manager.

diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py index 4b273a9cc81..ec108b696bf 100644 --- a/addons/hr_expense/__openerp__.py +++ b/addons/hr_expense/__openerp__.py @@ -24,7 +24,7 @@ 'name': 'Expense Management', 'version': '1.0', 'category': 'Human Resources', - 'sequence': 30, + 'sequence': 29, 'summary': 'Expenses Validation, Invoicing', 'description': """ Manage expenses by Employees diff --git a/addons/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py index c9961fa8a1d..ccb8af68e21 100644 --- a/addons/hr_expense/hr_expense.py +++ b/addons/hr_expense/hr_expense.py @@ -64,22 +64,22 @@ class hr_expense_expense(osv.osv): _description = "Expense" _order = "id desc" _columns = { - 'name': fields.char('Description', size=128), + 'name': fields.char('Description', size=128, required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}), 'id': fields.integer('Sheet ID', readonly=True), - 'date': fields.date('Date', select=True), + 'date': fields.date('Date', select=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}), 'journal_id': fields.many2one('account.journal', 'Force Journal', help = "The journal used when the expense is done."), - 'employee_id': fields.many2one('hr.employee', "Employee", required=True), + 'employee_id': fields.many2one('hr.employee', "Employee", required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}), 'user_id': fields.many2one('res.users', 'User', required=True), 'date_confirm': fields.date('Confirmation Date', select=True, help="Date of the confirmation of the sheet expense. It's filled when the button Confirm is pressed."), 'date_valid': fields.date('Validation Date', select=True, help="Date of the acceptation of the sheet expense. It's filled when the button Accept is pressed."), - 'user_valid': fields.many2one('res.users', 'Validation By'), + 'user_valid': fields.many2one('res.users', 'Validation By', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}), 'account_move_id': fields.many2one('account.move', 'Ledger Posting'), 'line_ids': fields.one2many('hr.expense.line', 'expense_id', 'Expense Lines', readonly=True, states={'draft':[('readonly',False)]} ), 'note': fields.text('Note'), 'amount': fields.function(_amount, string='Total Amount', digits_compute= dp.get_precision('Account')), 'voucher_id': fields.many2one('account.voucher', "Employee's Receipt"), - 'currency_id': fields.many2one('res.currency', 'Currency', required=True), - 'department_id':fields.many2one('hr.department','Department'), + 'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}), + 'department_id':fields.many2one('hr.department','Department', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}), 'company_id': fields.many2one('res.company', 'Company', required=True), 'state': fields.selection([ ('draft', 'New'), @@ -100,6 +100,12 @@ class hr_expense_expense(osv.osv): 'currency_id': _get_currency, } + def unlink(self, cr, uid, ids, context=None): + for rec in self.browse(cr, uid, ids, context=context): + if rec.state != 'draft': + raise osv.except_osv(_('Warning!'),_('You can only delete draft expenses!')) + return super(hr_expense_expense, self).unlink(cr, uid, ids, context) + def onchange_currency_id(self, cr, uid, ids, currency_id=False, company_id=False, context=None): res = {'value': {'journal_id': False}} journal_ids = self.pool.get('account.journal').search(cr, uid, [('type','=','purchase'), ('currency','=',currency_id), ('company_id', '=', company_id)], context=context) @@ -236,16 +242,6 @@ class product_product(osv.osv): 'hr_expense_ok': fields.boolean('Can be Expensed', help="Specify if the product can be selected in an HR expense line."), } - def on_change_hr_expense_ok(self, cr, uid, id, hr_expense_ok): - - if not hr_expense_ok: - return {} - data_obj = self.pool.get('ir.model.data') - cat_id = data_obj._get_id(cr, uid, 'hr_expense', 'cat_expense') - categ_id = data_obj.browse(cr, uid, cat_id).res_id - res = {'value' : {'type':'service','sale_ok' :False,'categ_id':categ_id }} - return res - product_product() class hr_expense_line(osv.osv): diff --git a/addons/hr_expense/hr_expense_data.xml b/addons/hr_expense/hr_expense_data.xml index 2a9f3ae0a56..f54156ac5ec 100644 --- a/addons/hr_expense/hr_expense_data.xml +++ b/addons/hr_expense/hr_expense_data.xml @@ -6,10 +6,10 @@ mail.group notification + Expense Management application installed! - Manage your employees' expenses, after due validation by their manager and the accountant, then generate and pay the corresponding invoices. - -This feature is also linked to analytic accounting and compatible with timesheet invoices, so you will be able to automatically re-invoice project-related expenses to your customers. + Manage your employees' expenses, after due validation by their manager and the accountant, then generate and pay the corresponding invoices.

+

This feature is also linked to analytic accounting and compatible with timesheet invoices, so you will be able to automatically re-invoice project-related expenses to your customers.

]]>
diff --git a/addons/hr_expense/hr_expense_view.xml b/addons/hr_expense/hr_expense_view.xml index dda977a8a93..d4b7cbae1ce 100644 --- a/addons/hr_expense/hr_expense_view.xml +++ b/addons/hr_expense/hr_expense_view.xml @@ -123,8 +123,8 @@
- - + + @@ -195,7 +195,7 @@
- +
@@ -211,8 +211,7 @@
- - + diff --git a/addons/hr_holidays/__openerp__.py b/addons/hr_holidays/__openerp__.py index 3cf4a30a2e1..417748d874b 100644 --- a/addons/hr_holidays/__openerp__.py +++ b/addons/hr_holidays/__openerp__.py @@ -25,7 +25,7 @@ 'version': '1.5', 'author': 'OpenERP SA', 'category': 'Human Resources', - 'sequence': 28, + 'sequence': 27, 'summary': 'Holidays, Allocation and Leave Requests', 'website': 'http://www.openerp.com', 'description': """ diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 0977f3729b4..92b1d57ce15 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -27,6 +27,7 @@ from operator import itemgetter import math import netsvc +import tools from osv import fields, osv from tools.translate import _ @@ -89,6 +90,18 @@ class hr_holidays_status(osv.osv): 'color_name': 'red', 'active': True, } + + def name_get(self, cr, uid, ids, context=None): + if not ids: + return [] + res = [] + for record in self.browse(cr, uid, ids, context=context): + name = record.name + if not record.limit: + name = name + (' (%d/%d)' % (record.leaves_taken or 0.0, record.max_leaves or 0.0)) + res.append((record.id, name)) + return res + hr_holidays_status() class hr_holidays(osv.osv): @@ -112,6 +125,13 @@ class hr_holidays(osv.osv): result[hol.id] = hol.number_of_days_temp return result + def _check_date(self, cr, uid, ids): + for holiday in self.browse(cr, uid, ids): + holiday_ids = self.search(cr, uid, [('date_from', '<=', holiday.date_to), ('date_to', '>=', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '<>', holiday.id)]) + if holiday_ids: + return False + return True + _columns = { 'name': fields.char('Description', size=64), 'state': fields.selection([('draft', 'To Submit'), ('cancel', 'Cancelled'),('confirm', 'To Approve'), ('refuse', 'Refused'), ('validate1', 'Second Approval'), ('validate', 'Approved')], @@ -145,12 +165,16 @@ class hr_holidays(osv.osv): 'user_id': lambda obj, cr, uid, context: uid, 'holiday_type': 'employee' } + _constraints = [ + (_check_date, 'You can not have 2 leaves that overlaps on same day!', ['date_from','date_to']), + ] + _sql_constraints = [ ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL))", "The employee or employee category of this request is missing."), - ('date_check2', "CHECK ( (type='add') OR (date_from <= date_to))", "The start date must be before the end date !"), - ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0 !"), + ('date_check2', "CHECK ( (type='add') OR (date_from <= date_to))", "The start date must be anterior to the end date."), + ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), ] - + def _create_resource_leave(self, cr, uid, leaves, context=None): '''This method will create entry in resource calendar leave object at the time of holidays validated ''' obj_res_leave = self.pool.get('resource.calendar.leaves') @@ -182,6 +206,13 @@ class hr_holidays(osv.osv): } return result + def onchange_employee(self, cr, uid, ids, employee_id): + result = {'value': {'department_id': False}} + if employee_id: + employee = self.pool.get('hr.employee').browse(cr, uid, employee_id) + result['value'] = {'department_id': employee.department_id.id} + return result + # TODO: can be improved using resource calendar method def _get_number_of_days(self, date_from, date_to): """Returns a float equals to the timedelta between two dates given as string.""" @@ -196,22 +227,62 @@ class hr_holidays(osv.osv): def unlink(self, cr, uid, ids, context=None): for rec in self.browse(cr, uid, ids, context=context): if rec.state not in ['draft', 'cancel', 'confirm']: - raise osv.except_osv(_('Warning!'),_('You cannot delete a leave which is in %s state!')%(rec.state)) + raise osv.except_osv(_('Warning!'),_('You cannot delete a leave which is in %s state.')%(rec.state)) return super(hr_holidays, self).unlink(cr, uid, ids, context) def onchange_date_from(self, cr, uid, ids, date_to, date_from): - result = {} - if date_to and date_from: + """ + If there are no date set for date_to, automatically set one 8 hours later than + the date_from. + Also update the number_of_days. + """ + # date_to has to be greater than date_from + if (date_from and date_to) and (date_from > date_to): + raise osv.except_osv(_('Warning!'),_('The start date must be anterior to the end date.')) + + result = {'value': {}} + + # No date_to set so far: automatically compute one 8 hours later + if date_from and not date_to: + date_to_with_delta = datetime.datetime.strptime(date_from, tools.DEFAULT_SERVER_DATETIME_FORMAT) + datetime.timedelta(hours=8) + result['value']['date_to'] = str(date_to_with_delta) + + # Compute and update the number of days + if (date_to and date_from) and (date_from <= date_to): diff_day = self._get_number_of_days(date_from, date_to) - result['value'] = { - 'number_of_days_temp': round(math.floor(diff_day))+1 - } - return result - result['value'] = { - 'number_of_days_temp': 0, - } + result['value']['number_of_days_temp'] = round(math.floor(diff_day))+1 + else: + result['value']['number_of_days_temp'] = 0 + return result + def onchange_date_to(self, cr, uid, ids, date_to, date_from): + """ + Update the number_of_days. + """ + + # date_to has to be greater than date_from + if (date_from and date_to) and (date_from > date_to): + raise osv.except_osv(_('Warning!'),_('The start date must be anterior to the end date.')) + + result = {'value': {}} + + # Compute and update the number of days + if (date_to and date_from) and (date_from <= date_to): + diff_day = self._get_number_of_days(date_from, date_to) + result['value']['number_of_days_temp'] = round(math.floor(diff_day))+1 + else: + result['value']['number_of_days_temp'] = 0 + + return result + + def write(self, cr, uid, ids, vals, context=None): + check_fnct = self.pool.get('hr.holidays.status').check_access_rights + for holiday in self.browse(cr, uid, ids, context=context): + if holiday.state in ('validate','validate1') and not check_fnct(cr, uid, 'write', raise_exception=False): + raise osv.except_osv(_('Warning!'),_('You cannot modify a leave request that has been approved. Contact a human resource manager.')) + return super(hr_holidays, self).write(cr, uid, ids, vals, context=context) + def set_to_draft(self, cr, uid, ids, context=None): self.write(cr, uid, ids, { 'state': 'draft', @@ -430,10 +501,10 @@ class hr_employee(osv.osv): def _get_remaining_days(self, cr, uid, ids, name, args, context=None): cr.execute("""SELECT sum(h.number_of_days) as days, - h.employee_id + h.employee_id from hr_holidays h - join hr_holidays_status s on (s.id=h.holiday_status_id) + join hr_holidays_status s on (s.id=h.holiday_status_id) where h.state='validate' and s.limit=False and @@ -448,9 +519,9 @@ class hr_employee(osv.osv): remaining[employee_id] = 0.0 return remaining - def _get_leave_status(self, cr, uid, ids, name, args, context=None): + def _get_leave_status(self, cr, uid, ids, name, args, context=None): holidays_obj = self.pool.get('hr.holidays') - holidays_id = holidays_obj.search(cr, uid, + holidays_id = holidays_obj.search(cr, uid, [('employee_id', 'in', ids), ('date_from','<=',time.strftime('%Y-%m-%d %H:%M:%S')), ('date_to','>=',time.strftime('%Y-%m-%d 23:59:59')),('type','=','remove'),('state','not in',('cancel','refuse'))], context=context) @@ -476,7 +547,7 @@ class hr_employee(osv.osv): ('validate1', 'Waiting Second Approval'), ('validate', 'Approved'), ('cancel', 'Cancelled')]), 'current_leave_id': fields.function(_get_leave_status, multi="leave_status", string="Current Leave Type",type='many2one', relation='hr.holidays.status'), 'leave_date_from': fields.function(_get_leave_status, multi='leave_status', type='date', string='From Date'), - 'leave_date_to': fields.function(_get_leave_status, multi='leave_status', type='date', string='To Date'), + 'leave_date_to': fields.function(_get_leave_status, multi='leave_status', type='date', string='To Date'), } hr_employee() diff --git a/addons/hr_holidays/hr_holidays_data.xml b/addons/hr_holidays/hr_holidays_data.xml index afe2e69e697..5af0c60fc44 100644 --- a/addons/hr_holidays/hr_holidays_data.xml +++ b/addons/hr_holidays/hr_holidays_data.xml @@ -11,9 +11,10 @@ mail.group notification + Leave Management application installed! - Manage employee leaves from the top menu "Human Resources". Employees can create leave requests that are validated by their manager and/or HR officers. -Once validated, they are visible in the employee's calendar. HR officers can define leave types and allocate leaves to employees and employee categories. + Manage employee leaves from the top menu "Human Resources". Employees can create leave requests that are validated by their manager and/or HR officers.

+

Once validated, they are visible in the employee's calendar. HR officers can define leave types and allocate leaves to employees and employee categories.

]]>
diff --git a/addons/hr_holidays/hr_holidays_demo.xml b/addons/hr_holidays/hr_holidays_demo.xml index 4c9d18683c7..accf8c26935 100644 --- a/addons/hr_holidays/hr_holidays_demo.xml +++ b/addons/hr_holidays/hr_holidays_demo.xml @@ -34,8 +34,8 @@ Summer Vacation - - + + add draft 7 @@ -45,8 +45,8 @@ International Tour - - + + add 7 diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml index e92679e0bad..752d278baed 100644 --- a/addons/hr_holidays/hr_holidays_view.xml +++ b/addons/hr_holidays/hr_holidays_view.xml @@ -13,9 +13,9 @@ action_holidays_unread - + action - + hr.holidays client_action_multi @@ -31,9 +31,9 @@ action_holidays_read - + action - + hr.holidays client_action_multi @@ -83,7 +83,7 @@ - + Leave Request hr.holidays 1 @@ -93,7 +93,7 @@ + + - @@ -83,7 +84,7 @@
- +
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index bd0ade31b68..9fd476d9d7f 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -25,6 +25,7 @@ import tools from email.header import decode_header from openerp import SUPERUSER_ID from openerp.osv import osv, orm, fields +from openerp.tools import html_email_clean from openerp.tools.translate import _ _logger = logging.getLogger(__name__) @@ -50,8 +51,9 @@ class mail_message(osv.Model): _description = 'Message' _inherit = ['ir.needaction_mixin'] _order = 'id desc' + _rec_name = 'record_name' - _message_read_limit = 10 + _message_read_limit = 30 _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from', 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids'] _message_record_name_length = 18 @@ -120,21 +122,26 @@ class mail_message(osv.Model): "message, comment for other messages such as user replies"), 'email_from': fields.char('From', help="Email address of the sender. This field is set when no matching partner is found for incoming emails."), - 'author_id': fields.many2one('res.partner', 'Author', + 'author_id': fields.many2one('res.partner', 'Author', select=1, + ondelete='set null', help="Author of the message. If not set, email_from may hold an email address that did not match any partner."), 'partner_ids': fields.many2many('res.partner', string='Recipients'), 'notified_partner_ids': fields.many2many('res.partner', 'mail_notification', - 'message_id', 'partner_id', 'Recipients'), + 'message_id', 'partner_id', 'Notified partners', + help='Partners that have a notification pushing this message in their mailboxes'), 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'), - 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."), + 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, + ondelete='set null', help="Initial thread message."), 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'), 'model': fields.char('Related Document Model', size=128, select=1), 'res_id': fields.integer('Related Document ID', select=1), 'record_name': fields.function(_get_record_name, type='char', store=True, string='Message Record Name', help="Name get of the related document."), - 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'), + 'notification_ids': fields.one2many('mail.notification', 'message_id', + string='Notifications', + help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'), 'subject': fields.char('Subject'), 'date': fields.datetime('Date'), 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1), @@ -142,7 +149,8 @@ class mail_message(osv.Model): 'to_read': fields.function(_get_to_read, fnct_search=_search_to_read, type='boolean', string='To read', help='Functional field to search for messages the current user has to read'), - 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'), + 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype', + ondelete='set null', select=1,), 'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes', help='Users that voted for this message'), @@ -200,67 +208,99 @@ class mail_message(osv.Model): # Message loading for web interface #------------------------------------------------------ - def _message_get_dict(self, cr, uid, message, context=None): - """ Return a dict representation of the message. This representation is - used in the JS client code, to display the messages. + def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None): + """ Post-processing on values given by message_read. This method will + handle partners in batch to avoid doing numerous queries. - :param dict message: read result of a mail.message + :param list messages: list of message, as get_dict result + :param dict message_tree: {[msg.id]: msg browse record} """ - # TDE note: this method should be optimized, to lessen the number of queries, will be done ASAP - is_author = False - if message['author_id']: - is_author = message['author_id'][0] == self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0] - author_id = message['author_id'] - elif message['email_from']: - author_id = (0, message['email_from']) + res_partner_obj = self.pool.get('res.partner') + ir_attachment_obj = self.pool.get('ir.attachment') + pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0] - has_voted = False - if uid in message.get('vote_user_ids'): - has_voted = True + # 1. Aggregate partners (author_id and partner_ids) and attachments + partner_ids = set() + attachment_ids = set() + for key, message in message_tree.iteritems(): + if message.author_id: + partner_ids |= set([message.author_id.id]) + if message.partner_ids: + partner_ids |= set([partner.id for partner in message.partner_ids]) + if message.attachment_ids: + attachment_ids |= set([attachment.id for attachment in message.attachment_ids]) - is_favorite = False - if uid in message.get('favorite_user_ids'): - is_favorite = True + # Filter author_ids uid can see + # partner_ids = self.pool.get('res.partner').search(cr, uid, [('id', 'in', partner_ids)], context=context) + partners = res_partner_obj.name_get(cr, uid, list(partner_ids), context=context) + partner_tree = dict((partner[0], partner) for partner in partners) - is_private = True - if message.get('model') and message.get('res_id'): - is_private = False + # 2. Attachments + attachments = ir_attachment_obj.read(cr, uid, list(attachment_ids), ['id', 'datas_fname'], context=context) + attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname']}) for attachment in attachments) - try: - attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, message['attachment_ids'], context=context)] - except (orm.except_orm, osv.except_osv): - attachment_ids = [] - - # TDE note: should we send partner_ids ? - # TDE note: shouldn't we separated followers and other partners ? costly to compute maybe , - try: - partner_ids = self.pool.get('res.partner').name_get(cr, uid, message['partner_ids'], context=context) - except (orm.except_orm, osv.except_osv): + # 3. Update message dictionaries + for message_dict in messages: + message_id = message_dict.get('id') + message = message_tree[message_id] + if message.author_id: + author = partner_tree[message.author_id.id] + else: + author = (0, message.email_from) partner_ids = [] + for partner in message.partner_ids: + if partner.id in partner_tree: + partner_ids.append(partner_tree[partner.id]) + attachment_ids = [] + for attachment in message.attachment_ids: + if attachment.id in attachments_tree: + attachment_ids.append(attachments_tree[attachment.id]) + message_dict.update({ + 'is_author': pid == author[0], + 'author_id': author, + 'partner_ids': partner_ids, + 'attachment_ids': attachment_ids, + }) + return True - return { - 'id': message['id'], - 'type': message['type'], - 'attachment_ids': attachment_ids, - 'body': message['body'], - 'model': message['model'], - 'res_id': message['res_id'], - 'record_name': message['record_name'], - 'subject': message['subject'], - 'date': message['date'], - 'author_id': author_id, - 'is_author': is_author, - 'partner_ids': partner_ids, - 'parent_id': False, - 'vote_nb': len(message['vote_user_ids']), - 'has_voted': has_voted, - 'is_private': is_private, - 'is_favorite': is_favorite, - 'to_read': message['to_read'], - } + def _message_read_dict(self, cr, uid, message, parent_id=False, context=None): + """ Return a dict representation of the message. This representation is + used in the JS client code, to display the messages. Partners and + attachments related stuff will be done in post-processing in batch. - def _message_read_add_expandables(self, cr, uid, message_list, read_messages, - thread_level=0, message_loaded_ids=[], domain=[], parent_id=False, context=None, limit=None): + :param dict message: mail.message browse record + """ + # private message: no model, no res_id + is_private = False + if not message.model or not message.res_id: + is_private = True + # votes and favorites: res.users ids, no prefetching should be done + vote_nb = len(message.vote_user_ids) + has_voted = uid in [user.id for user in message.vote_user_ids] + is_favorite = uid in [user.id for user in message.favorite_user_ids] + + return {'id': message.id, + 'type': message.type, + 'body': html_email_clean(message.body), + 'model': message.model, + 'res_id': message.res_id, + 'record_name': message.record_name, + 'subject': message.subject, + 'date': message.date, + 'to_read': message.to_read, + 'parent_id': parent_id, + 'is_private': is_private, + 'author_id': False, + 'is_author': False, + 'partner_ids': [], + 'vote_nb': vote_nb, + 'has_voted': has_voted, + 'is_favorite': is_favorite, + 'attachment_ids': [], + } + + def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree, + message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None): """ Create expandables for message_read, to load new messages. 1. get the expandable for new threads if display is flat (thread_level == 0): @@ -275,96 +315,82 @@ class mail_message(osv.Model): for each hole in the child list based on message displayed, create an expandable - :param list message_list:list of message structure for the Chatter + :param list messages: list of message structure for the Chatter widget to which expandables are added - :param dict read_messages: dict [id]: read result of the messages to - easily have access to their values, given their ID + :param dict message_tree: dict [id]: browse record of this message + :param dict parent_tree: dict [parent_id]: [child_ids] + :param list message_unload_ids: list of message_ids we do not want + to load :return bool: True """ - def _get_expandable(domain, message_nb, parent_id, id, model): + def _get_expandable(domain, message_nb, parent_id, max_limit): return { 'domain': domain, 'nb_messages': message_nb, 'type': 'expandable', 'parent_id': parent_id, - 'id': id, - # TDE note: why do we need model sometimes, and sometimes not ??? - 'model': model, + 'max_limit': max_limit, } - # all_not_loaded_ids = [] - id_list = sorted(read_messages.keys()) - if not id_list: - return message_list + if not messages: + return True + message_ids = sorted(message_tree.keys()) # 1. get the expandable for new threads if thread_level == 0: - exp_domain = domain + [('id', '<', min(message_loaded_ids + id_list))] + exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))] else: - exp_domain = domain + ['!', ('id', 'child_of', message_loaded_ids + id_list)] + exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())] ids = self.search(cr, uid, exp_domain, context=context, limit=1) if ids: - message_list.append(_get_expandable(exp_domain, -1, parent_id, -1, None)) + # inside a thread: prepend + if parent_id: + messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True)) + # new threads: append + else: + messages.append(_get_expandable(exp_domain, -1, parent_id, True)) # 2. get the expandables for new messages inside threads if display is not flat if thread_level == 0: return True - for message_id in id_list: - message = read_messages[message_id] + for message_id in message_ids: + message = message_tree[message_id] - # message is not a thread header (has a parent_id) - # TDE note: parent_id is false is there is a parent we can not see -> ok - if message.get('parent_id'): + # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok) + if message.parent_id: continue - # TDE note: check search is correctly implemented in mail.message - not_loaded_ids = self.search(cr, uid, [ - ('id', 'child_of', message['id']), - ('id', 'not in', message_loaded_ids), - ], context=context, limit=self._message_read_more_limit) - if not not_loaded_ids: + # check there are message for expandable + child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids) + child_ids = sorted(list(child_ids), reverse=True) + if not child_ids: continue - # all_not_loaded_ids += not_loaded_ids - # group childs not read - id_min, id_max, nb = max(not_loaded_ids), 0, 0 - for not_loaded_id in not_loaded_ids: - if not read_messages.get(not_loaded_id): + # make groups of unread messages + id_min, id_max, nb = max(child_ids), 0, 0 + for child_id in child_ids: + if not child_id in message_ids: nb += 1 - if id_min > not_loaded_id: - id_min = not_loaded_id - if id_max < not_loaded_id: - id_max = not_loaded_id + if id_min > child_id: + id_min = child_id + if id_max < child_id: + id_max = child_id elif nb > 0: exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model'))) - id_min, id_max, nb = max(not_loaded_ids), 0, 0 + messages.append(_get_expandable(exp_domain, nb, message_id, False)) + id_min, id_max, nb = max(child_ids), 0, 0 else: - id_min, id_max, nb = max(not_loaded_ids), 0, 0 + id_min, id_max, nb = max(child_ids), 0, 0 if nb > 0: exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model'))) - - # message_loaded_ids = list(set(message_loaded_ids + read_messages.keys() + all_not_loaded_ids)) + idx = [msg.get('id') for msg in messages].index(message_id) + 1 + # messages.append(_get_expandable(exp_domain, nb, message_id, id_min)) + messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False)) return True - def _get_parent(self, cr, uid, message, context=None): - """ Tools method that tries to get the parent of a mail.message. If - no parent, or if uid has no access right on the parent, False - is returned. - - :param dict message: read result of a mail.message - """ - if not message['parent_id']: - return False - parent_id = message['parent_id'][0] - try: - return self.read(cr, uid, parent_id, self._message_read_fields, context=context) - except (orm.except_orm, osv.except_osv): - return False - - def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None, thread_level=0, context=None, parent_id=False, limit=None): + def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None, + thread_level=0, context=None, parent_id=False, limit=None): """ Read messages from mail.message, and get back a list of structured messages to be displayed as discussion threads. If IDs is set, fetch these records. Otherwise use the domain to fetch messages. @@ -388,46 +414,56 @@ class mail_message(osv.Model): ancestors and expandables :return list: list of message structure for the Chatter widget """ - # print 'message_read', ids, domain, message_unload_ids, thread_level, context, parent_id, limit assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level domain = domain if domain is not None else [] message_unload_ids = message_unload_ids if message_unload_ids is not None else [] if message_unload_ids: domain += [('id', 'not in', message_unload_ids)] limit = limit or self._message_read_limit - read_messages = {} + message_tree = {} message_list = [] + parent_tree = {} # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to if ids is None: ids = self.search(cr, uid, domain, context=context, limit=limit) - for message in self.read(cr, uid, ids, self._message_read_fields, context=context): - message_id = message['id'] - # if not in tree and not in message_loaded list - if not message_id in read_messages and not message_id in message_unload_ids: - read_messages[message_id] = message - message_list.append(self._message_get_dict(cr, uid, message, context=context)) + # fetch parent if threaded, sort messages + for message in self.browse(cr, uid, ids, context=context): + message_id = message.id + if message_id in message_tree: + continue + message_tree[message_id] = message - # get the older ancestor the user can read, update its ancestor field - if not thread_level: - message_list[-1]['parent_id'] = parent_id - continue - parent = self._get_parent(cr, uid, message, context=context) - while parent and parent.get('id') != parent_id: - message_list[-1]['parent_id'] = parent.get('id') - message = parent - parent = self._get_parent(cr, uid, message, context=context) - # if in thread: add its ancestor to the list of messages - if not message['id'] in read_messages and not message['id'] in message_unload_ids: - read_messages[message['id']] = message - message_list.append(self._message_get_dict(cr, uid, message, context=context)) + # find parent_id + if thread_level == 0: + tree_parent_id = parent_id + else: + tree_parent_id = message_id + parent = message + while parent.parent_id and parent.parent_id.id != parent_id: + parent = parent.parent_id + tree_parent_id = parent.id + if not parent.id in message_tree: + message_tree[parent.id] = parent + # newest messages first + parent_tree.setdefault(tree_parent_id, []) + if tree_parent_id != message_id: + parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context)) + + if thread_level: + for key, message_id_list in parent_tree.iteritems(): + message_id_list.sort(key=lambda item: item['id']) + message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context)) + + parent_list = parent_tree.items() + parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True) + message_list = [message for (key, msg_list) in parent_list for message in msg_list] # get the child expandable messages for the tree - message_list = sorted(message_list, key=lambda k: k['id']) - self._message_read_add_expandables(cr, uid, message_list, read_messages, thread_level=thread_level, - message_loaded_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context, limit=limit) - + self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context) + self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree, + thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context) return message_list # TDE Note: do we need this ? @@ -461,7 +497,6 @@ class mail_message(osv.Model): - otherwise: remove the id """ # Rules do not apply to administrator - # print '_search', uid, args if uid == SUPERUSER_ID: return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count, access_rights_uid=access_rights_uid) @@ -599,6 +634,8 @@ class mail_message(osv.Model): def create(self, cr, uid, values, context=None): if not values.get('message_id') and values.get('res_id') and values.get('model'): values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values) + elif not values.get('message_id'): + values['message_id'] = tools.generate_tracking_message_id('private') newid = super(mail_message, self).create(cr, uid, values, context) self._notify(cr, SUPERUSER_ID, newid, context=context) return newid @@ -729,7 +766,7 @@ class mail_message(osv.Model): ], context=context) fol_objs = fol_obj.read(cr, uid, fol_ids, ['partner_id'], context=context) partners_to_notify |= set(fol['partner_id'][0] for fol in fol_objs) - # when writing to a wall + # remove me from notified partners, unless the message is written on my own wall if message.get('author_id') and message.get('model') == "res.partner" and message.get('res_id') == message.get('author_id')[0]: partners_to_notify |= set([message.get('author_id')[0]]) elif message.get('author_id'): diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index 93b35797fd5..7d2e9483773 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -59,21 +59,26 @@ + + - + +
@@ -84,7 +89,6 @@ form tree,form - {'search_default_to_read_message':True} diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index f3203336615..ee1a015396b 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -74,17 +74,17 @@ class mail_thread(osv.AbstractModel): - message_unread: has uid unread message for the document - message_summary: html snippet summarizing the Chatter for kanban views """ res = dict((id, dict(message_unread=False, message_summary='')) for id in ids) + user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] - # search for unread messages, by reading directly mail.notification, as SUPERUSER - notif_obj = self.pool.get('mail.notification') - notif_ids = notif_obj.search(cr, SUPERUSER_ID, [ - ('partner_id.user_ids', 'in', [uid]), - ('message_id.res_id', 'in', ids), - ('message_id.model', '=', self._name), - ('read', '=', False) - ], context=context) - for notif in notif_obj.browse(cr, SUPERUSER_ID, notif_ids, context=context): - res[notif.message_id.res_id]['message_unread'] = True + # search for unread messages, directly in SQL to improve performances + cr.execute(""" SELECT m.res_id FROM mail_message m + RIGHT JOIN mail_notification n + ON (n.message_id = m.id AND n.partner_id = %s AND n.read = False) + WHERE m.model = %s AND m.res_id in %s""", + (user_pid, self._name, tuple(ids),)) + msg_ids = [result[0] for result in cr.fetchall()] + for msg_id in msg_ids: + res[msg_id]['message_unread'] = True for thread in self.browse(cr, uid, ids, context=context): cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or '' @@ -319,10 +319,12 @@ class mail_thread(osv.AbstractModel): """ assert isinstance(message, Message), 'message must be an email.message.Message at this point' message_id = message.get('Message-Id') + references = decode_header(message, 'References') + in_reply_to = decode_header(message, 'In-Reply-To') # 1. Verify if this is a reply to an existing thread - references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To') - ref_match = references and tools.reference_re.search(references) + thread_references = references or in_reply_to + ref_match = thread_references and tools.reference_re.search(thread_references) if ref_match: thread_id = int(ref_match.group(1)) model = ref_match.group(2) or model @@ -333,6 +335,14 @@ class mail_thread(osv.AbstractModel): message_id, model, thread_id, custom_values, uid) return [(model, thread_id, custom_values, uid)] + # Verify this is a reply to a private message + message_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', in_reply_to)], limit=1, context=context) + if message_ids: + message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context) + _logger.debug('Routing mail with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s', + message_id, message.id, custom_values, uid) + return [(False, 0, custom_values, uid)] + # 2. Look for a matching mail.alias entry # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value. @@ -376,14 +386,19 @@ class mail_thread(osv.AbstractModel): def message_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, thread_id=None, context=None): - """Process an incoming RFC2822 email message, relying on - ``mail.message.parse()`` for the parsing operation, - and ``message_route()`` to figure out the target model. + """ Process an incoming RFC2822 email message, relying on + ``mail.message.parse()`` for the parsing operation, + and ``message_route()`` to figure out the target model. - Once the target model is known, its ``message_new`` method - is called with the new message (if the thread record did not exist) + Once the target model is known, its ``message_new`` method + is called with the new message (if the thread record did not exist) or its ``message_update`` method (if it did). + There is a special case where the target model is False: a reply + to a private message. In this case, we skip the message_new / + message_update step, to just post a new message using mail_thread + message_post. + :param string model: the fallback model to use if the message does not match any of the currently configured mail aliases (may be None if a matching alias is supposed to be present) @@ -425,15 +440,19 @@ class mail_thread(osv.AbstractModel): for model, thread_id, custom_values, user_id in routes: if self._name != model: context.update({'thread_model': model}) - model_pool = self.pool.get(model) - assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ - "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \ - (msg['message_id'], model) - if thread_id and hasattr(model_pool, 'message_update'): - model_pool.message_update(cr, user_id, [thread_id], msg, context=context) + if model: + model_pool = self.pool.get(model) + assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ + "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \ + (msg['message_id'], model) + if thread_id and hasattr(model_pool, 'message_update'): + model_pool.message_update(cr, user_id, [thread_id], msg, context=context) + else: + thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context) else: - thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context) - model_pool.message_post(cr, uid, [thread_id], context=context, **msg) + assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." + model_pool = self.pool.get('mail.thread') + model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg) return thread_id def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): @@ -501,7 +520,7 @@ class mail_thread(osv.AbstractModel): body = tools.ustr(body, encoding, errors='replace') if message.get_content_type() == 'text/plain': # text/plain ->
-                body = tools.append_content_to_html(u'', body)
+                body = tools.append_content_to_html(u'', body, preserve=True)
         else:
             alternative = (message.get_content_type() == 'multipart/alternative')
             for part in message.walk():
@@ -516,7 +535,7 @@ class mail_thread(osv.AbstractModel):
                 # 2) text/plain -> 
                 if part.get_content_type() == 'text/plain' and (not alternative or not body):
                     body = tools.append_content_to_html(body, tools.ustr(part.get_payload(decode=True),
-                                                                         encoding, errors='replace'))
+                                                                         encoding, errors='replace'), preserve=True)
                 # 3) text/html -> raw
                 elif part.get_content_type() == 'text/html':
                     html = tools.ustr(part.get_payload(decode=True), encoding, errors='replace')
@@ -556,7 +575,6 @@ class mail_thread(osv.AbstractModel):
         """
         msg_dict = {
             'type': 'email',
-            'subtype': 'mail.mt_comment',
             'author_id': False,
         }
         if not isinstance(message, Message):
@@ -588,7 +606,7 @@ class mail_thread(osv.AbstractModel):
             else:
                 msg_dict['email_from'] = message.get('from')
         partner_ids = self._message_find_partners(cr, uid, message, ['From', 'To', 'Cc'], context=context)
-        msg_dict['partner_ids'] = partner_ids
+        msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
 
         if 'Date' in message:
             date_hdr = decode(message.get('Date'))
@@ -629,7 +647,8 @@ class mail_thread(osv.AbstractModel):
             mail.message ID. Extra keyword arguments will be used as default
             column values for the new mail.message record.
             Auto link messages for same id and object
-            :param int thread_id: thread ID to post into, or list with one ID
+            :param int thread_id: thread ID to post into, or list with one ID;
+                if False/0, mail.message model will also be set as False
             :param str body: body of the message, usually raw HTML that will
                 be sanitized
             :param str subject: optional subject
@@ -639,10 +658,13 @@ class mail_thread(osv.AbstractModel):
                 ``(name,content)``, where content is NOT base64 encoded
             :return: ID of newly created mail.message
         """
-        context = context or {}
-        attachments = attachments or []
+        if context is None:
+            context = {}
+        if attachments is None:
+            attachments = {}
+
         assert (not thread_id) or isinstance(thread_id, (int, long)) or \
-            (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id"
+            (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id; should be 0, False, an ID or a list with one ID"
         if isinstance(thread_id, (list, tuple)):
             thread_id = thread_id and thread_id[0]
         mail_message = self.pool.get('mail.message')
@@ -682,7 +704,6 @@ class mail_thread(osv.AbstractModel):
             # avoid loops when finding ancestors
             processed_list = []
             if message_ids:
-                _counter, _counter_max = 0, 200
                 message = mail_message.browse(cr, SUPERUSER_ID, message_ids[0], context=context)
                 while (message.parent_id and message.parent_id.id not in processed_list):
                     processed_list.append(message.parent_id.id)
@@ -707,18 +728,45 @@ class mail_thread(osv.AbstractModel):
 
         return mail_message.create(cr, uid, values, context=context)
 
-    def message_post_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False, attachment_ids=None, context=None):
-        """ Wrapper on message_post, used only in Chatter (JS). The purpose is
-            to handle attachments.
-            # TDE FIXME: body is plaintext: convert it into html
+    def message_post_user_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False,
+                                attachment_ids=None, context=None, content_subtype='plaintext', **kwargs):
+        """ Wrapper on message_post, used for user input :
+            - mail gateway
+            - quick reply in Chatter (refer to mail.js), not
+                the mail.compose.message wizard
+            The purpose is to perform some pre- and post-processing:
+            - if body is plaintext: convert it into html
+            - if parent_id: handle reply to a previous message by adding the
+                parent partners to the message
+            - type and subtype: comment and mail.mt_comment by default
+            - attachment_ids: supposed not attached to any document; attach them
+                to the related document. Should only be set by Chatter.
         """
-        new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type='comment',
-                        subtype='mail.mt_comment', parent_id=parent_id, context=context)
+        ir_attachment = self.pool.get('ir.attachment')
+        mail_message = self.pool.get('mail.message')
 
-        # HACK FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
+        # 1. Pre-processing: body, partner_ids, type and subtype
+        if content_subtype == 'plaintext':
+            body = tools.plaintext2html(body)
+
+        partner_ids = kwargs.pop('partner_ids', [])
+        if parent_id:
+            parent_message = self.pool.get('mail.message').browse(cr, uid, parent_id, context=context)
+            partner_ids += [(4, partner.id) for partner in parent_message.partner_ids]
+            # TDE FIXME HACK: mail.thread -> private message
+            if self._name == 'mail.thread' and parent_message.author_id.id:
+                partner_ids.append((4, parent_message.author_id.id))
+
+        message_type = kwargs.pop('type', 'comment')
+        message_subtype = kwargs.pop('subtype', 'mail.mt_comment')
+
+        # 2. Post message
+        new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=message_type,
+                        subtype=message_subtype, parent_id=parent_id, context=context, partner_ids=partner_ids, **kwargs)
+
+        # 3. Post-processing
+        # HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
         if attachment_ids:
-            ir_attachment = self.pool.get('ir.attachment')
-            mail_message = self.pool.get('mail.message')
             filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
                 ('res_model', '=', 'mail.compose.message'),
                 ('res_id', '=', 0),
diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml
index 07b24725b05..3a37218c813 100644
--- a/addons/mail/mail_thread_view.xml
+++ b/addons/mail/mail_thread_view.xml
@@ -4,48 +4,126 @@
         
             Inbox
             mail.wall
-            
+            mail.message
+            {
+              'default_model': 'res.users', 
+              'default_res_id': uid,
+            }
+            
+            
+                

+ Good Job! Your inbox is empty. +

+ Your inbox contains private messages or emails sent to you + as well as information related to documents or people you + follow. +

+
To: me mail.wall - + mail.message + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'search_default_message_unread': True + } + + +

+ No private message. +

+ This list contains messages sent to you. +

+
- Favorites + Todo mail.wall - + mail.message + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'search_default_message_unread': True + } + + +

+ No todo! +

+ When you process messages in your inbox, you can mark some + as todo. From this menu, you can process all your todo. +

+
Archives mail.wall - + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'search_default_message_read': True + } + + +

+ No message found. +

+
Sent mail.wall - + { + 'default_model': 'res.users', + 'default_res_id': uid + } + + +

+ No message sent yet. +

+ Click on the top-right icon to compose a message. This + message will be sent by email if it's an internal contact. +

+
- - + @@ -61,7 +139,7 @@ - Favorites + Todo diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py index 6b13fe6ad20..060adc291de 100644 --- a/addons/mail/res_partner.py +++ b/addons/mail/res_partner.py @@ -42,4 +42,23 @@ class res_partner_mail(osv.Model): 'notification_email_send': lambda *args: 'comment' } + def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', + subtype=None, parent_id=False, attachments=None, context=None, **kwargs): + """ Override related to res.partner. In case of email message, set it as + private: + - add the target partner in the message partner_ids + - set thread_id as None, because this will trigger the 'private' + aspect of the message (model=False, res_id=False) + """ + if isinstance(thread_id, (list, tuple)): + thread_id = thread_id[0] + if type == 'email': + partner_ids = kwargs.get('partner_ids', []) + if thread_id not in partner_ids: + partner_ids.append((4, thread_id)) + kwargs['partner_ids'] = partner_ids + thread_id = False + return super(res_partner_mail, self).message_post(cr, uid, thread_id, body=body, subject=subject, + type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index 1b6388111ba..20c0715c148 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -91,8 +91,10 @@ class res_users(osv.Model): return user_id def _create_welcome_message(self, cr, uid, user, context=None): - company_name = user.company_id.name if user.company_id else _('the company') - body = _('%s has joined %s.') % (user.name, company_name) + if not self.has_group(cr, uid, 'base.group_user'): + return False + company_name = user.company_id.name if user.company_id else '' + body = _('%s has joined the %s network.') % (user.name, company_name) # TODO change SUPERUSER_ID into user.id but catch errors return self.pool.get('res.partner').message_post(cr, SUPERUSER_ID, [user.partner_id.id], body=body, context=context) @@ -111,7 +113,8 @@ class res_users(osv.Model): alias_pool.unlink(cr, uid, alias_ids, context=context) return res - def message_post_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False, attachment_ids=None, context=None): + def message_post_user_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False, + attachment_ids=None, context=None, content_subtype='plaintext', **kwargs): """ Redirect the posting of message on res.users to the related partner. This is done because when giving the context of Chatter on the various mailboxes, we do not have access to the current partner_id. @@ -122,8 +125,8 @@ class res_users(osv.Model): if isinstance(thread_id, (list, tuple)): thread_id = thread_id[0] partner_id = self.pool.get('res.users').read(cr, uid, thread_id, ['partner_id'], context=context)['partner_id'][0] - return self.pool.get('res.partner').message_post_api(cr, uid, partner_id, body=body, subject=subject, - parent_id=parent_id, attachment_ids=attachment_ids, context=context) + return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, body=body, subject=subject, + parent_id=parent_id, attachment_ids=attachment_ids, context=context, content_subtype=content_subtype, **kwargs) def message_post(self, cr, uid, thread_id, context=None, **kwargs): """ Redirect the posting of message on res.users to the related partner. @@ -143,6 +146,7 @@ class res_users(osv.Model): return self.pool.get('res.partner').message_update(cr, uid, [partner_id], msg_dict, update_vals=update_vals, context=context) + class res_users_mail_group(osv.Model): """ Update of res.users class - if adding groups to an user, check mail.groups linked to this user diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 52c6c605109..37c6952717c 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -7,12 +7,15 @@ access_mail_mail_system,mail.mail.system,model_mail_mail,base.group_system,1,1,1 access_mail_followers_all,mail.followers.all,model_mail_followers,,1,0,0,0 access_mail_followers_system,mail.followers.system,model_mail_followers,base.group_system,1,1,1,1 access_mail_notification_all,mail.notification.all,model_mail_notification,,1,0,0,0 -access_mail_notification_aystem,mail.notification.system,model_mail_notification,base.group_system,1,1,1,1 +access_mail_notification_group_user,mail.notification.user,model_mail_notification,base.group_user,1,1,1,0 +access_mail_notification_system,mail.notification.system,model_mail_notification,base.group_system,1,1,1,1 access_mail_group_all,mail.group.all,model_mail_group,,1,0,0,0 access_mail_group_user,mail.group.user,model_mail_group,base.group_user,1,1,1,1 access_mail_alias_all,mail.alias.all,model_mail_alias,,1,0,0,0 -access_mail_alias_user,mail.alias,model_mail_alias,base.group_user,1,1,1,0 -access_mail_alias_system,mail.alias,model_mail_alias,base.group_system,1,1,1,1 +access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,1,1,0 +access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1 access_mail_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0 access_mail_vote_all,mail.vote.all,model_mail_vote,,1,1,1,1 -access_mail_favorite_all,mail.favorite.all,model_mail_favorite,,1,1,1,1 \ No newline at end of file +access_mail_favorite_all,mail.favorite.all,model_mail_favorite,,1,1,1,1 +access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1 +access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1 diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml index 3f2b7bf3e9a..525425c2aa0 100644 --- a/addons/mail/security/mail_security.xml +++ b/addons/mail/security/mail_security.xml @@ -10,7 +10,7 @@ ['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.partner_id.id]), '&', ('public','=','groups'), ('group_public_id','in', [g.id for g in user.groups_id])] - + mail.followers: read and write its own entries [('partner_id', '=', user.partner_id.id)] @@ -18,6 +18,14 @@ + + mail.notification: read and write its own entries + + [('partner_id', '=', user.partner_id.id)] + + + + - + +
@@ -38,7 +39,8 @@
-