[MERGE] hr_expense: Usability review + Mail gateway integration with alias_contacs + Fixes

Merge branch 'saas-12-expense-fixes-yti' into saas-12
This commit is contained in:
Yannick Tivisse
2016-08-25 16:13:04 +02:00
10 changed files with 147 additions and 137 deletions
+11
View File
@@ -29,5 +29,16 @@ Leave Management (keep track of employee leaves), Expense Management (manage emp
<field name="image" eval="obj(ref('base.partner_root')).image" model="res.partner"/>
</record>
<record id="mail_template_data_unknown_employee_email_address" model="mail.template">
<field name="name">HR: unknown employee bounce</field>
<field name="subject">Your document has not been created</field>
<field name="email_from">${user.email}</field>
<field name="email_to">${ctx['email_to']|safe}</field>
<field name="lang">${user.lang}</field>
<field name="model_id" ref="base.model_ir_module_module"/>
<field name="auto_delete" eval="True"/>
<field name="body_html" ><![CDATA[Your document has not been created because your email address is not recognized. Please send emails with the email address recorded on your employee information, or contact your HR manager.]]></field>
</record>
</data>
</odoo>
+2
View File
@@ -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
+10
View File
@@ -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')])
+55
View File
@@ -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
+11 -14
View File
@@ -17,6 +17,7 @@
<field name="alias_name">expense</field>
<field name="alias_model_id" ref="model_hr_expense"/>
<field name="alias_user_id" ref="base.user_root"/>
<field name="alias_contact">employees</field>
</record>
@@ -41,18 +42,24 @@
<field name="name">Approved</field>
<field name="res_model">hr.expense.sheet</field>
<field name="default" eval="True"/>
<field name="description">Expense approved</field>
<field name="description">Expense report approved</field>
</record>
<record id="mt_expense_refused" model="mail.message.subtype">
<field name="name">Refused</field>
<field name="res_model">hr.expense.sheet</field>
<field name="default" eval="True"/>
<field name="description">Expense refused</field>
<field name="description">Expense report refused</field>
</record>
<record id="mt_expense_confirmed" model="mail.message.subtype">
<field name="name">To Approve</field>
<field name="name">Submitted</field>
<field name="res_model">hr.expense.sheet</field>
<field name="description">Expense confirmed, waiting confirmation</field>
<field name="description">Expense report submitted, waiting approval</field>
<field name="default" eval="False"/>
</record>
<record id="mt_expense_paid" model="mail.message.subtype">
<field name="name">Paid</field>
<field name="res_model">hr.expense.sheet</field>
<field name="description">Expense report paid</field>
<field name="default" eval="False"/>
</record>
<!-- Department (Parent) related subtypes for messaging / Chatter -->
@@ -65,15 +72,5 @@
<field name="sequence" eval="10"/>
</record>
<record id="mail_template_data_expense_unknown_email_address" model="mail.template">
<field name="name">HrExpense: unknown employee bounce</field>
<field name="subject">Your expense has not been created</field>
<field name="email_from">help@odoo.com</field>
<field name="email_to">${ctx['email_to']|safe}</field>
<field name="lang">${user.lang}</field>
<field name="model_id" ref="base.model_ir_module_module"/>
<field name="auto_delete" eval="True"/>
<field name="body_html" ><![CDATA[Your expense has not been created because your email address is not set on an employee or on a employee's user. Configure your employee's information correctly and try again.]]></field>
</record>
</data>
</odoo>
+17 -33
View File
@@ -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 <bar>'. 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.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (sheet.name, reason))
sheet.message_post(body=body)
@api.multi
def paid_expense_sheets(self):
self.write({'state': 'done'})
-15
View File
@@ -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
+39 -72
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_expenses_tree" model="ir.ui.view">
<field name="name">hr.expense.tree</field>
<field name="model">hr.expense</field>
@@ -187,7 +188,7 @@
<record id="hr_expense_actions_all" model="ir.actions.act_window">
<field name="name">All Expenses</field>
<field name="res_model">hr.expense</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="view_mode">pivot,graph,tree,kanban,form</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
@@ -204,26 +205,6 @@
</field>
</record>
<record id="hr_expense_actions_my_all" model="ir.actions.act_window">
<field name="name">My Expenses</field>
<field name="res_model">hr.expense</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
<field name="context">{'show_report': True}</field>
<field name="help" type="html">
<p>
The steps to process expenses are:
<ol>
<li>You record expenses,</li>
<li>Periodically, you submit expenses report to your manager,</li>
<li>Once approved, your expense report, goes to the accounting,</li>
<li>The accountant validates entries and reimburse you.</li>
</ol>
</p>
</field>
</record>
<record id="hr_expense_actions_my_unsubmitted" model="ir.actions.act_window">
<field name="name">My Expenses to Report</field>
<field name="res_model">hr.expense</field>
@@ -244,25 +225,24 @@
</field>
</record>
<record id="hr_expense_tree_action" model="ir.actions.act_window.view">
<field name="sequence" eval="0"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_expenses_tree"/>
<field name="act_window_id" ref="hr_expense_actions_all"/>
</record>
<record id="hr_expense_kanban_action" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="hr_expense_kanban_view"/>
<field name="act_window_id" ref="hr_expense_actions_all"/>
</record>
<record id="hr_expense_form_action" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="hr_expense_form_view"/>
<field name="act_window_id" ref="hr_expense_actions_all"/>
<record id="action_hr_expense_refused_expenses" model="ir.actions.act_window">
<field name="name">Refused Expenses</field>
<field name="res_model">hr.expense</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
<field name="context">{'search_default_refused': 1}</field>
<field name="help" type="html">
<p>
The steps to process expenses are:
<ol>
<li>You record expenses,</li>
<li>Periodically, you submit expenses report to your manager,</li>
<li>Once approved, your expense report, goes to the accounting,</li>
<li>The accountant validates entries and reimburse you.</li>
</ol>
</p>
</field>
</record>
<record id="view_product_hr_expense_form" model="ir.ui.view">
@@ -412,6 +392,7 @@
<field name="name" string="Expense Sheet"/>
<field name="accounting_date"/>
<field name="employee_id"/>
<field name="currency_id" invisible="1"/>
<field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="state"/>
<field name="message_unread" invisible="1"/>
@@ -578,6 +559,7 @@
<filter string="Department" domain="[]" context="{'group_by': 'department_id'}" groups="hr.group_multi_departments"/>
<filter string="Company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
<filter string="Expenses Month" domain="[]" context="{'group_by': 'accounting_date'}" help="Expenses by Month"/>
<filter string="State" domain="[]" context="{'group_by': 'state'}" name="state"/>
</group>
<separator />
<filter domain="[('employee_id.active', '=', False)]" string="Former Employees" name="inactive" groups="base.group_hr_user,base.group_hr_manager"/>
@@ -585,22 +567,6 @@
</field>
</record>
<record id="action_hr_expense_sheet_my_to_approve" model="ir.actions.act_window">
<field name="name">My Reports</field>
<field name="res_model">hr.expense.sheet</field>
<field name="view_mode">tree,kanban,form,pivot,graph</field>
<field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
<field name="context">{'search_default_submitted': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new expense report.
</p><p>
Once you have created your expense, submit it to your manager who will validate it.
</p>
</field>
</record>
<record id="action_hr_expense_sheet_my_all" model="ir.actions.act_window">
<field name="name">My Reports</field>
<field name="res_model">hr.expense.sheet</field>
@@ -668,7 +634,7 @@
<record id="action_hr_expense_sheet_all_all" model="ir.actions.act_window">
<field name="name">My Reports</field>
<field name="res_model">hr.expense.sheet</field>
<field name="view_mode">tree,kanban,form,pivot,graph</field>
<field name="view_mode">pivot,graph,tree,kanban,form</field>
<field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
<field name="domain">[]</field>
<field name="context">{}</field>
@@ -778,23 +744,24 @@
<menuitem id="menu_hr_expense_root" name="Expenses" sequence="100" web_icon="hr_expense,static/description/icon.png"/>
<menuitem id="menu_hr_expense_my_expenses" name="My Expenses" sequence="1" parent="menu_hr_expense_root"/>
<menuitem id="menu_hr_expense_my_unsubmitted" name="My Expenses to Report" sequence="10" parent="menu_hr_expense_my_expenses" action="hr_expense_actions_my_unsubmitted"/>
<menuitem id="menu_hr_expense_my_all" name="All Expenses" sequence="2" parent="menu_hr_expense_my_expenses"
action="hr_expense_actions_my_all"/>
<menuitem id="menu_hr_expense_my_expenses" name="My Expenses" sequence="1" parent="menu_hr_expense_root" groups="base.group_user"/>
<menuitem id="menu_hr_expense_my_expenses_to_submit" name="Expenses to Submit" sequence="1" parent="menu_hr_expense_my_expenses" action="hr_expense_actions_my_unsubmitted"/>
<menuitem id="menu_hr_expense_my_expenses_refused" name="Refused Expenses" sequence="2" parent="menu_hr_expense_my_expenses" action="action_hr_expense_refused_expenses"/>
<menuitem id="menu_hr_expense_sheet_my_reports" name="Expense Reports" sequence="3" parent="menu_hr_expense_my_expenses" action="action_hr_expense_sheet_my_all" load_xmlid="True"/>
<menuitem id="menu_hr_expense_sheet_my_reports" name="My Reports" sequence="15" parent="menu_hr_expense_root"/>
<menuitem id="menu_hr_expense_sheet_my_to_approve" name="To Approve" sequence="1" parent="menu_hr_expense_sheet_my_reports" action="action_hr_expense_sheet_my_to_approve"/>
<menuitem id="menu_hr_expense_to_approve" name="To Approve" sequence="2" parent="menu_hr_expense_root" groups="base.group_hr_user"/>
<menuitem id="menu_hr_expense_sheet_all_to_approve" name="Expense Reports To Approve" sequence="1" parent="menu_hr_expense_to_approve"
action="action_hr_expense_sheet_all_to_approve"/>
<menuitem id="menu_hr_expense_accountant" name="Accountant" sequence="3" parent="menu_hr_expense_root" groups="base.group_hr_manager"/>
<menuitem id="menu_hr_expense_sheet_all_to_post" name="Expense Reports To Post" sequence="2" parent="menu_hr_expense_accountant"
action="action_hr_expense_sheet_all_to_post"/>
<menuitem id="menu_hr_expense_sheet_all_to_pay" name="Expense Reports To Pay" sequence="3" parent="menu_hr_expense_accountant"
action="action_hr_expense_sheet_all_to_pay"/>
<menuitem id="menu_hr_expense_sheet_my_all" name="All Reports" sequence="2" parent="menu_hr_expense_sheet_my_reports" action="action_hr_expense_sheet_my_all" load_xmlid="True"/>
<menuitem id="menu_hr_expense_sheet_all" name="All Reports" sequence="20" parent="menu_hr_expense_root"/>
<menuitem id="menu_hr_expense_sheet_all_to_approve" name="Reports To Approve" sequence="1" parent="menu_hr_expense_sheet_all"
action="action_hr_expense_sheet_all_to_approve" groups="base.group_hr_user"/>
<menuitem id="menu_hr_expense_sheet_all_to_post" name="Reports To Post" sequence="2" parent="menu_hr_expense_sheet_all"
action="action_hr_expense_sheet_all_to_post" groups="base.group_hr_manager"/>
<menuitem id="menu_hr_expense_sheet_all_to_pay" name="Reports To Pay" sequence="3" parent="menu_hr_expense_sheet_all"
action="action_hr_expense_sheet_all_to_pay" groups="base.group_hr_manager"/>
<menuitem id="menu_hr_expense_sheet_all_all" name="All Reports" sequence="4" parent="menu_hr_expense_sheet_all"
<menuitem id="menu_hr_expense_reports" name="Reports" sequence="4" parent="menu_hr_expense_root" groups="base.group_hr_manager"/>
<menuitem id="menu_hr_expense_all_expenses" name="All Expenses" sequence="0" parent="menu_hr_expense_reports" action="hr_expense_actions_all"/>
<menuitem id="menu_hr_expense_sheet_all_all" name="All Expense Reports" sequence="4" parent="menu_hr_expense_reports"
action="action_hr_expense_sheet_all_all" groups="base.group_hr_manager"/>
<menuitem id="menu_hr_expense_configuration" name="Configuration" parent="menu_hr_expense_root"
@@ -12,7 +12,6 @@
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
</header>
<group string="Expenses" name="config_alias">
<field name="alias_expense_open" widget="radio"/>
<label for="id" string="Expense Email Alias"/>
<div>
<div attrs="{'invisible': [('alias_domain', 'in', ['localhost', '', False])]}">
@@ -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 <a href='/mail/view?%s'>%s</a> 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