[IMP] hr_expense: add draft state to expense lines

We need to be able to change an expense report before submitting it, for
this we add a new step in the expense process (a draft state for the
expense sheets).

Task #41700
Closes #24806
This commit is contained in:
RomainLibert
2018-06-07 13:55:06 +02:00
parent d4b5668c1a
commit 631891dc5d
5 changed files with 54 additions and 29 deletions
+22 -8
View File
@@ -82,7 +82,7 @@ class HrExpense(models.Model):
@api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state')
def _compute_state(self):
for expense in self:
if not expense.sheet_id:
if not expense.sheet_id or expense.sheet_id.state == 'draft':
expense.state = "draft"
elif expense.sheet_id.state == "cancel":
expense.state = "refused"
@@ -177,7 +177,7 @@ class HrExpense(models.Model):
@api.multi
def action_submit_expenses(self):
if any(expense.state != 'draft' for expense in self):
if any(expense.state != 'draft' or expense.sheet_id 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."))
@@ -508,19 +508,20 @@ class HrExpenseSheet(models.Model):
name = fields.Char('Expense Report Summary', required=True)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
state = fields.Selection([
('draft', 'Draft'),
('submit', 'Submitted'),
('approve', 'Approved'),
('post', 'Posted'),
('done', 'Paid'),
('cancel', 'Refused')
], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='submit', required=True, help='Expense Report State')
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1))
], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='draft', required=True, help='Expense Report State')
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1))
address_id = fields.Many2one('res.partner', string="Employee Home Address")
payment_mode = fields.Selection([("own_account", "Employee (to reimburse)"), ("company_account", "Company")], related='expense_line_ids.payment_mode', default='own_account', readonly=True, string="Paid By")
user_id = fields.Many2one('res.users', 'Manager', readonly=True, copy=False, states={'submit': [('readonly', False)]}, track_visibility='onchange', oldname='responsible_id')
user_id = fields.Many2one('res.users', 'Manager', readonly=True, copy=False, states={'draft': [('readonly', False)]}, track_visibility='onchange', oldname='responsible_id')
total_amount = fields.Monetary('Total Amount', currency_field='currency_id', compute='_compute_amount', store=True, digits=dp.get_precision('Account'))
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id)
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id)
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id)
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id)
attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments')
journal_id = fields.Many2one('account.journal', string='Expense Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, default=_default_journal_id, 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=_default_bank_journal_id, help="The payment method used when the expense is paid by the company.")
@@ -528,6 +529,7 @@ class HrExpenseSheet(models.Model):
account_move_id = fields.Many2one('account.move', string='Journal Entry', ondelete='restrict', copy=False)
department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
is_multiple_currency = fields.Boolean("Handle lines with different currencies", compute='_compute_is_multiple_currency')
can_reset = fields.Boolean('Can Reset', compute='_compute_can_reset')
@api.depends('expense_line_ids.total_amount_company')
def _compute_amount(self):
@@ -544,6 +546,12 @@ class HrExpenseSheet(models.Model):
for sheet in self:
sheet.is_multiple_currency = len(sheet.expense_line_ids.mapped('currency_id')) > 1
@api.multi
def _compute_can_reset(self):
is_expense_user = self.user_has_groups('hr_expense.group_hr_expense_user')
for sheet in self:
sheet.can_reset = is_expense_user if is_expense_user else sheet.employee_id.user_id == self.env.user
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.address_id = self.employee_id.sudo().address_home_id
@@ -647,6 +655,10 @@ class HrExpenseSheet(models.Model):
def set_to_paid(self):
self.write({'state': 'done'})
@api.multi
def action_submit_sheet(self):
self.write({'state': 'submit'})
@api.multi
def approve_expense_sheets(self):
if not self.user_has_groups('hr_expense.group_hr_expense_user'):
@@ -688,8 +700,10 @@ class HrExpenseSheet(models.Model):
@api.multi
def reset_expense_sheets(self):
if not self.can_reset:
raise UserError(_("Only HR Officers or the concerned employee can reset to draft."))
self.mapped('expense_line_ids').write({'is_refused': False})
self.write({'state': 'submit'})
self.write({'state': 'draft'})
self.activity_update()
return True
@@ -1,9 +1,10 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_expense_user,hr.expense.user,model_hr_expense,hr_expense.group_hr_expense_user,1,1,1,1
access_hr_expense_employee,hr.expense.employee,model_hr_expense,base.group_user,1,1,1,1
access_hr_expense_sheet_manager,hr.expense.sheet.manager,model_hr_expense_sheet,hr_expense.group_hr_expense_manager,1,1,1,1
access_hr_expense_sheet_user,hr.expense.sheet.user,model_hr_expense_sheet,hr_expense.group_hr_expense_user,1,1,1,1
access_hr_expense_sheet_employee,hr.expense.sheet.employee,model_hr_expense_sheet,base.group_user,1,1,1,1
access_hr_expense_user,hr.expense.user,model_hr_expense,hr_expense.group_hr_expense_user,1,1,1,1
access_hr_expense_sheet_user,hr.expense.sheet.user,model_hr_expense_sheet,hr_expense.group_hr_expense_user,1,1,1,1
access_hr_expense_manager,hr.expense.manager,model_hr_expense,hr_expense.group_hr_expense_manager,1,1,1,1
access_hr_expense_sheet_manager,hr.expense.sheet.manager,model_hr_expense_sheet,hr_expense.group_hr_expense_manager,1,1,1,1
access_product_product_hr_expense_user,product.product.hr.expense.user,product.model_product_product,hr_expense.group_hr_expense_user,1,1,1,1
access_product_template_hr_expense_user,product.template.hr.expense.user,product.model_product_template,hr_expense.group_hr_expense_user,1,1,1,1
access_uom_uom_hr_expense_user,uom.uom.hr.expense.user,uom.model_uom_uom,hr_expense.group_hr_expense_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
access_hr_expense_user hr.expense.user model_hr_expense hr_expense.group_hr_expense_user 1 1 1 1
2 access_hr_expense_employee hr.expense.employee model_hr_expense base.group_user 1 1 1 1
access_hr_expense_sheet_manager hr.expense.sheet.manager model_hr_expense_sheet hr_expense.group_hr_expense_manager 1 1 1 1
access_hr_expense_sheet_user hr.expense.sheet.user model_hr_expense_sheet hr_expense.group_hr_expense_user 1 1 1 1
3 access_hr_expense_sheet_employee hr.expense.sheet.employee model_hr_expense_sheet base.group_user 1 1 1 1
4 access_hr_expense_user hr.expense.user model_hr_expense hr_expense.group_hr_expense_user 1 1 1 1
5 access_hr_expense_sheet_user hr.expense.sheet.user model_hr_expense_sheet hr_expense.group_hr_expense_user 1 1 1 1
6 access_hr_expense_manager hr.expense.manager model_hr_expense hr_expense.group_hr_expense_manager 1 1 1 1
7 access_hr_expense_sheet_manager hr.expense.sheet.manager model_hr_expense_sheet hr_expense.group_hr_expense_manager 1 1 1 1
8 access_product_product_hr_expense_user product.product.hr.expense.user product.model_product_product hr_expense.group_hr_expense_user 1 1 1 1
9 access_product_template_hr_expense_user product.template.hr.expense.user product.model_product_template hr_expense.group_hr_expense_user 1 1 1 1
10 access_uom_uom_hr_expense_user uom.uom.hr.expense.user uom.model_uom_uom hr_expense.group_hr_expense_user 1 1 1 1
+14 -11
View File
@@ -15,20 +15,19 @@ tour.register('hr_expense_tour' ,
trigger: '.o_app[data-menu-xmlid="hr_expense.menu_hr_expense_root"], .oe_menu_toggler[data-menu-xmlid="hr_expense.menu_hr_expense_root"]',
content: _t("Want to manage your expenses? It starts here."),
position: 'bottom',
}, {
trigger: '.o_list_button_add',
extra_trigger: ".o_expense_tree",
content: _t('<p>Let’s start by creating your first expense.</p>'),
position: 'bottom'
}, {
trigger: '.o_form_button_save',
extra_trigger: ".o_expense_sheet",
content: _t("<p>Once your <b>Expense report</b> is ready, you can save it and wait for the approval from your manager.</p>"),
content: _t("<p>Once your <b> Expense </b> is ready, you can save it.</p>"),
position: 'bottom',
}, {
trigger: '.o_expense_form .o_chatter_button_new_message',
trigger: '.o_attach_document',
content: _t("Attach your receipt here."),
position: 'top',
position: 'bottom',
}, {
trigger: '.o_expense_submit',
extra_triggger: ".o_expense_form",
content: _t('<p>Click on <b> Create Report </b> to create the report.</p>'),
position: 'right',
}, {
trigger: '.o_expense_tree .o_checkbox > input[type=checkbox]',
content: _t('<p>Select expenses to submit them to your manager</p>'),
@@ -36,11 +35,15 @@ tour.register('hr_expense_tour' ,
}, {
trigger: '.o_dropdown_toggler_btn',
extra_trigger: ".o_expense_tree",
content: _t('<p>Click on <b> Action Submit To Manager </b> to submit selected expenses to your manager</p>'),
content: _t('<p>Click on <b> Action Create Report </b> to submit selected expenses to your manager</p>'),
position: 'right',
}, {
trigger: '.o_expense_sheet_submit',
content: _t('Once your <b>Expense report</b> is ready, you can submit it to your manager and wait for the approval from your manager.'),
position: 'bottom',
}, {
trigger: '.o_expense_sheet_approve',
content: _t("<p>Approve the sheet here.</p><p>Tip: if you refuse, don’t forget to give the reason thanks to the hereunder message tool</p>"),
content: _t("<p>Approve the report here.</p><p>Tip: if you refuse, don’t forget to give the reason thanks to the hereunder message tool</p>"),
position: 'bottom',
}, {
trigger: '.o_expense_sheet_post',
+3
View File
@@ -43,7 +43,10 @@ class TestAccountEntry(TestExpenseCommon):
'analytic_account_id': self.analytic_account.id,
})
expense_line._onchange_product_id()
# State should default to draft
self.assertEquals(expense.state, 'draft', 'Expense should be created in Draft state')
# Submitted to Manager
expense.action_submit_sheet()
self.assertEquals(expense.state, 'submit', 'Expense is not in Reported state')
# Approve
expense.approve_expense_sheets()
+11 -7
View File
@@ -43,10 +43,10 @@
<header>
<widget name="attach_document" string="Attach Document" action="message_post" attrs="{'invisible': [('attachment_number', '&lt;=', 0)]}"/>
<widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '&gt;=', 1)]}"/>
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&gt;=', 1), ('sheet_id', '!=', False)]}"/>
<button name="action_submit_expenses" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&lt;=', 0), ('sheet_id', '!=', False)]}"/>
<button name="action_submit_expenses" string="Create Report" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&gt;=', 1), ('sheet_id', '!=', False)]}"/>
<button name="action_submit_expenses" string="Create Report" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&lt;=', 0), ('sheet_id', '!=', False)]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,reported,approved,done,refused"/>
<button name="action_view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
<button name="action_view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('sheet_id', '=', False)]}"/>
</header>
<sheet>
<div class="oe_button_box">
@@ -200,6 +200,7 @@
<filter domain="[('state', '=', 'refused')]" string="Refused" name="refused" help="Refused Expenses"/>
<filter domain="['|', ('state', '=', 'approved'), ('state', '=', 'done')]" string="Approved" name="approved" help="Actual expense sheets, not the refused ones"/>
<separator />
<filter domain="[('sheet_id', '=', False)]" string="Not in a report" name="no_report"/>
<filter string="My Team Expenses" name="my_team_expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="hr_expense.group_hr_expense_manager" help="Expenses of Your Team Member"/>
<filter string="My Expenses" name="my_expenses" domain="[('employee_id.user_id', '=', uid)]"/>
<separator />
@@ -245,7 +246,7 @@
<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="context">{'search_default_to_report': 1, 'search_default_my_expenses': 1}</field>
<field name="context">{'search_default_to_report': 1, 'search_default_my_expenses': 1, 'search_default_no_report': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new expense
@@ -401,7 +402,7 @@
<field name="model">hr.expense.sheet</field>
<field name="arch" type="xml">
<tree string="Expense Reports" decoration-warning="state=='draft'" decoration-bf="message_unread == True" decoration-danger="state=='cancel'">
<field name="name" string="Expense Sheet"/>
<field name="name" string="Expense Report"/>
<field name="accounting_date"/>
<field name="employee_id"/>
<field name="currency_id" invisible="1"/>
@@ -418,11 +419,13 @@
<field eval="25" name="priority"/>
<field name="arch" type="xml">
<form string="Expense Reports" class="o_expense_sheet">
<field name="can_reset" invisible="1"/>
<header>
<button name="action_submit_sheet" states="draft" string="Submit to Manager" type="object" class="oe_highlight o_expense_sheet_submit"/>
<button name="approve_expense_sheets" states="submit" string="Approve" type="object" groups="hr_expense.group_hr_expense_user" class="oe_highlight o_expense_sheet_approve"/>
<button name="action_sheet_move_create" states="approve" string="Post Journal Entries" type="object" groups="account.group_account_manager" class="oe_highlight o_expense_sheet_post"/>
<button name="%(hr_expense.hr_expense_sheet_register_payment_wizard_action)d" type="action" string="Register Payment" class="oe_highlight o_expense_sheet_pay" attrs="{'invisible': [('state', '!=', 'post')]}" context="{'default_amount': total_amount, 'partner_id': address_id}" groups="account.group_account_manager"/>
<button name="reset_expense_sheets" states="cancel" string="Resubmit" type="object"/>
<button name="reset_expense_sheets" string="Reset to Draft" type="object" attrs="{'invisible': ['|', ('can_reset', '=', False), ('state', 'not in', ['submit', 'cancel'])]}"/>
<button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit,approve" context="{'hr_expense_refuse_model':'hr.expense.sheet'}" string="Refuse" type="action" groups="hr_expense.group_hr_expense_user" />
<field name="state" widget="statusbar" statusbar_visible="draft,submit,approve,post,done"/>
</header>
@@ -578,6 +581,7 @@
<field name="department_id" string="Department"/>
<field name="journal_id"/>
<separator />
<filter domain="[('state', '=', 'draft')]" string="Draft" name="draft" help="Expenses in Draft"/>
<filter domain="[('state', '=', 'submit')]" string="To Approve" name="submitted" help="Confirmed Expenses"/>
<filter domain="[('state', '=', 'approve')]" string="To Post" name="to_post" help="Approved Expenses"/>
<filter domain="['|', ('state', '=', 'approve'), ('state', '=', 'post')]" string="To Pay" name="approved" help="Expenses to Invoice"/>
@@ -777,7 +781,7 @@
</record>
<record id="hr_expense_submit_action_server" model="ir.actions.server">
<field name="name">Submit To Manager</field>
<field name="name">Create Report</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_hr_expense"/>
<field name="binding_model_id" ref="model_hr_expense"/>