diff --git a/addons/hr/data/hr_data.xml b/addons/hr/data/hr_data.xml
index bb2cf4b5a08..2b031146e01 100644
--- a/addons/hr/data/hr_data.xml
+++ b/addons/hr/data/hr_data.xml
@@ -29,5 +29,16 @@ Leave Management (keep track of employee leaves), Expense Management (manage emp
+
+ HR: unknown employee bounce
+ Your document has not been created
+ ${user.email}
+ ${ctx['email_to']|safe}
+ ${user.lang}
+
+
+
+
+
diff --git a/addons/hr/models/__init__.py b/addons/hr/models/__init__.py
index c870236ee6c..ef90454a6d7 100644
--- a/addons/hr/models/__init__.py
+++ b/addons/hr/models/__init__.py
@@ -2,5 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hr
+import mail_alias
+import mail_thread
import res_partner
import res_users
diff --git a/addons/hr/models/mail_alias.py b/addons/hr/models/mail_alias.py
new file mode 100644
index 00000000000..710b7efaff7
--- /dev/null
+++ b/addons/hr/models/mail_alias.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class Alias(models.Model):
+ _inherit = 'mail.alias'
+
+ alias_contact = fields.Selection(selection_add=[('employees', 'Authenticated Employees')])
diff --git a/addons/hr/models/mail_thread.py b/addons/hr/models/mail_thread.py
new file mode 100644
index 00000000000..d27cbbd2afc
--- /dev/null
+++ b/addons/hr/models/mail_thread.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+
+from odoo import api, models
+from odoo.addons.mail.models.mail_thread import decode_header
+from odoo.tools import email_split
+
+_logger = logging.getLogger(__name__)
+
+class MailThread(models.AbstractModel):
+ _inherit = "mail.thread"
+
+ @api.model
+ def message_route_verify(self, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False):
+ res = super(MailThread, self).message_route_verify(message, message_dict, route, update_author, assert_model, create_fallback, allow_private)
+
+ if res:
+ alias = route[4]
+ email_from = decode_header(message, 'From')
+ message_id = message.get('Message-Id')
+
+ # Identically equal to the definition in mail module because sub methods are local
+ # variables and cannot be called with super
+ def _create_bounce_email(body_html):
+ bounce_to = decode_header(message, 'Return-Path') or email_from
+ bounce_mail_values = {
+ 'body_html': body_html,
+ 'subject': 'Re: %s' % message.get('subject'),
+ 'email_to': bounce_to,
+ 'auto_delete': True,
+ }
+ bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
+ if bounce_from:
+ bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
+ self.env['mail.mail'].create(bounce_mail_values).send()
+
+ def _warn(message):
+ _logger.info('Routing mail with Message-Id %s: route %s: %s',
+ message_id, route, message)
+
+ # Alias: check alias_contact settings for employees
+
+ if alias and alias.alias_contact == 'employees':
+ email_address = email_split(email_from)[0]
+ employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
+ if not employee:
+ employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
+ if not employee:
+ mail_template = self.env.ref('hr.mail_template_data_unknown_employee_email_address')
+ _warn('alias %s does not accept unknown employees, skipping' % alias.alias_name)
+ _create_bounce_email(mail_template.body_html)
+ return False
+ return res
diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml
index 3ca4fed609c..e378419120d 100644
--- a/addons/hr_expense/data/hr_expense_data.xml
+++ b/addons/hr_expense/data/hr_expense_data.xml
@@ -17,6 +17,7 @@
expense
+ employees
@@ -41,18 +42,24 @@
Approved
hr.expense.sheet
- Expense approved
+ Expense report approved
Refused
hr.expense.sheet
- Expense refused
+ Expense report refused
- To Approve
+ Submitted
hr.expense.sheet
- Expense confirmed, waiting confirmation
+ Expense report submitted, waiting approval
+
+
+
+ Paid
+ hr.expense.sheet
+ Expense report paid
@@ -65,15 +72,5 @@
-
- HrExpense: unknown employee bounce
- Your expense has not been created
- help@odoo.com
- ${ctx['email_to']|safe}
- ${user.lang}
-
-
-
-
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 87273168687..a3c70a5f53f 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -33,13 +33,14 @@ class HrExpense(models.Model):
description = fields.Text()
payment_mode = fields.Selection([("own_account", "Employee (to reimburse)"), ("company_account", "Company")], default='own_account', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, string="Payment By")
attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments')
- state = fields.Selection([('draft', 'To Report'),
- ('reported', 'Reported'),
- ('done', 'Posted'),
- ('refused', 'Refused')
- ], compute='_compute_state', default="draft", string='Status', index=True, readonly=True, copy=False, required=True, store=True,
+ state = fields.Selection([
+ ('draft', 'To Report'),
+ ('reported', 'Reported'),
+ ('done', 'Posted'),
+ ('refused', 'Refused')
+ ], compute='_compute_state', string='Status', copy=False, default="draft", index=True, readonly=True, required=True, store=True,
help="Status of the expense.")
- sheet_id = fields.Many2one('hr.expense.sheet', string="Expense Report", readonly=True)
+ sheet_id = fields.Many2one('hr.expense.sheet', string="Expense Report", readonly=True, copy=False)
reference = fields.Char(string="Bill Reference")
@api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state')
@@ -100,6 +101,8 @@ class HrExpense(models.Model):
def submit_expenses(self):
if any(expense.state != 'draft' for expense in self):
raise UserError(_("You cannot report twice the same line!"))
+ if len(self.mapped('employee_id')) != 1:
+ raise UserError(_("You cannot report expenses for different employees in the same report!"))
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
@@ -290,26 +293,13 @@ class HrExpense(models.Model):
if custom_values is None:
custom_values = {}
- # Retrieve the email address from the email field. The string is constructed like
- # 'foo '. We will extract 'bar' from this
email_address = email_split(msg_dict.get('email_from', False))[0]
- # Look after an employee who has this email address or an employee for whom the related
- # user has this email address. In the case not employee is found, we send back an email
- # to explain that the expense will not be created.
- employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
- if not employee:
- employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
- if not employee:
- access = self.env['ir.config_parameter'].get_param("hr_expense.email.gateway") or 'open'
- if access == 'open':
- # Open access is mostly used during the on boarding flow, should not be used in production
- employee = self.env['hr.employee'].search([('user_id','<>',False)], limit=1)
- if not employee:
- # Send back an email to explain why the expense has not been created
- mail_template = self.env.ref('hr_expense.mail_template_data_expense_unknown_email_address')
- mail_template.with_context(email_to=email_address).send_mail(self.env.ref('base.module_hr_expense').id)
- return False
+ employee = self.env['hr.employee'].search([
+ '|',
+ ('work_email', 'ilike', email_address),
+ ('user_id.email', 'ilike', email_address)
+ ], limit=1)
expense_description = msg_dict.get('subject', '')
@@ -381,7 +371,7 @@ class HrExpenseSheet(models.Model):
help="The journal used when the expense is done.")
bank_journal_id = fields.Many2one('account.journal', string='Bank Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, default=lambda self: self.env['account.journal'].search([('type', 'in', ['case', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company.")
accounting_date = fields.Date(string="Accounting Date")
- account_move_id = fields.Many2one('account.move', string='Journal Entry', copy=False, track_visibility="onchange")
+ account_move_id = fields.Many2one('account.move', string='Journal Entry', copy=False)
department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
@api.multi
@@ -422,6 +412,8 @@ class HrExpenseSheet(models.Model):
return 'hr_expense.mt_expense_confirmed'
elif 'state' in init_values and self.state == 'cancel':
return 'hr_expense.mt_expense_refused'
+ elif 'state' in init_values and self.state == 'done':
+ return 'hr_expense.mt_expense_paid'
return super(HrExpenseSheet, self)._track_subtype(init_values)
def _add_followers(self):
@@ -474,16 +466,8 @@ class HrExpenseSheet(models.Model):
@api.multi
def approve_expense_sheets(self):
- self.message_post(body=_("The expense has been validated by %s") % (self.env.user.name))
self.write({'state': 'approve', 'responsible_id': self.env.user.id})
- @api.multi
- def refuse_expense_sheets(self, reason):
- self.write({'state': 'cancel'})
- for sheet in self:
- body = (_("Your Expense %s has been refused. ") % (sheet.name, reason))
- sheet.message_post(body=body)
-
@api.multi
def paid_expense_sheets(self):
self.write({'state': 'done'})
diff --git a/addons/hr_expense/models/res_config.py b/addons/hr_expense/models/res_config.py
index 71bc01b0861..c1eda4f459b 100644
--- a/addons/hr_expense/models/res_config.py
+++ b/addons/hr_expense/models/res_config.py
@@ -1,29 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
-from openerp import SUPERUSER_ID
class HrExpenseConfigSettings(models.TransientModel):
_name = 'hr.expense.config.settings'
_inherit = 'res.config.settings'
- alias_expense_open = fields.Selection([
- ('open', "Accept expenses from any email address"),
- ('restricted', "Accept expenses from employee email addresses only")
- ], "Mail Gateway")
-
alias_prefix = fields.Char('Default Alias Name for Expenses')
alias_domain = fields.Char('Alias Domain', default=lambda self: self.env["ir.config_parameter"].get_param("mail.catchall.domain"))
- @api.model
- def get_default_alias_expense_open(self, fields):
- return {'alias_expense_open': self.env['ir.config_parameter'].get_param("hr_expense.email.gateway") or 'open'}
-
- @api.multi
- def set_default_alias_expense_open(self):
- self.env['ir.config_parameter'].set_param("hr_expense.email.gateway", self.alias_expense_open)
- return True
-
@api.model
def get_default_alias_prefix(self, fields):
alias_name = self.env.ref('hr_expense.mail_alias_expense').alias_name
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index 830e87fb3a7..13c564280f6 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -1,5 +1,6 @@
+
hr.expense.tree
hr.expense
@@ -187,7 +188,7 @@
All Expenses
hr.expense
- tree,kanban,form,graph,pivot
+ pivot,graph,tree,kanban,form
@@ -204,26 +205,6 @@
-
- My Expenses
- hr.expense
- tree,kanban,form,graph,pivot
-
- [('employee_id.user_id', '=', uid)]
- {'show_report': True}
-
-
- The steps to process expenses are:
-
- You record expenses,
- Periodically, you submit expenses report to your manager,
- Once approved, your expense report, goes to the accounting,
- The accountant validates entries and reimburse you.
-
-
-
-
-
My Expenses to Report
hr.expense
@@ -244,25 +225,24 @@
-
-
- tree
-
-
-
-
-
-
- kanban
-
-
-
-
-
-
- form
-
-
+
+ Refused Expenses
+ hr.expense
+ tree,kanban,form,graph,pivot
+
+ [('employee_id.user_id', '=', uid)]
+ {'search_default_refused': 1}
+
+
+ The steps to process expenses are:
+
+ You record expenses,
+ Periodically, you submit expenses report to your manager,
+ Once approved, your expense report, goes to the accounting,
+ The accountant validates entries and reimburse you.
+
+
+
@@ -412,6 +392,7 @@
+
@@ -578,6 +559,7 @@
+
@@ -585,22 +567,6 @@
-
- My Reports
- hr.expense.sheet
- tree,kanban,form,pivot,graph
-
- [('employee_id.user_id', '=', uid)]
- {'search_default_submitted': 1}
-
-
- Click here to create a new expense report.
-
- Once you have created your expense, submit it to your manager who will validate it.
-
-
-
-
My Reports
hr.expense.sheet
@@ -668,7 +634,7 @@
My Reports
hr.expense.sheet
- tree,kanban,form,pivot,graph
+ pivot,graph,tree,kanban,form
[]
{}
@@ -778,23 +744,24 @@
-
-
-
+
+
+
+
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
diff --git a/addons/hr_expense/wizard/hr_expense_register_payment.py b/addons/hr_expense/wizard/hr_expense_register_payment.py
index 8e8fa7bce17..e5fe925fc7e 100644
--- a/addons/hr_expense/wizard/hr_expense_register_payment.py
+++ b/addons/hr_expense/wizard/hr_expense_register_payment.py
@@ -3,7 +3,7 @@
from odoo import api, fields, models, _
from openerp.exceptions import ValidationError
-
+from werkzeug import url_encode
class HrExpenseRegisterPaymentWizard(models.TransientModel):
@@ -76,7 +76,7 @@ class HrExpenseRegisterPaymentWizard(models.TransientModel):
payment.post()
# Log the payment in the chatter
- body = (_("A payment of %s %s with the reference %s related to your expense %s has been made.") % (payment.amount, payment.currency_id.symbol, payment.name, expense_sheet.name))
+ body = (_("A payment of %s %s with the reference
%s related to your expense %s has been made.") % (payment.amount, payment.currency_id.symbol, url_encode({'model': 'account.payment', 'res_id': payment.id}), payment.name, expense_sheet.name))
expense_sheet.message_post(body=body)
# Reconcile the payment and the expense, i.e. lookup on the payable account move lines