[IMP] hr_expense: show confirmation on duplicate expenses
Display a confirmation dialog when a user creates duplicate expenses. Two expenses (or more) are considered duplicates when they share the same date, amount, product, employee, company and currency. closes odoo/odoo#64190 Taskid: 2368636 Related: odoo/upgrade#2061 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
committed by
Yannick Tivisse
parent
b85bde9854
commit
278f5a6995
@@ -36,6 +36,7 @@ This module also uses analytic accounting and is compatible with the invoice on
|
||||
'data/hr_expense_sequence.xml',
|
||||
'data/hr_expense_data.xml',
|
||||
'wizard/hr_expense_refuse_reason_views.xml',
|
||||
'wizard/hr_expense_approve_duplicate_views.xml',
|
||||
'views/hr_expense_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
'security/ir_rule.xml',
|
||||
|
||||
@@ -98,6 +98,8 @@ class HrExpense(models.Model):
|
||||
('refused', 'Refused')
|
||||
], compute='_compute_state', string='Status', copy=False, index=True, readonly=True, store=True, default='draft', help="Status of the expense.")
|
||||
sheet_id = fields.Many2one('hr.expense.sheet', string="Expense Report", domain="[('employee_id', '=', employee_id), ('company_id', '=', company_id)]", readonly=True, copy=False)
|
||||
approved_by = fields.Many2one('res.users', string='Approved By', related='sheet_id.user_id')
|
||||
approved_on = fields.Datetime(string='Approved On', related='sheet_id.approval_date')
|
||||
reference = fields.Char("Bill Reference")
|
||||
is_refused = fields.Boolean("Explicitly Refused by manager or accountant", readonly=True, copy=False)
|
||||
|
||||
@@ -105,6 +107,8 @@ class HrExpense(models.Model):
|
||||
is_ref_editable = fields.Boolean("Reference Is Editable By Current User", compute='_compute_is_ref_editable')
|
||||
product_has_cost = fields.Boolean("Is product with non zero cost selected", compute='_compute_product_has_cost')
|
||||
same_currency = fields.Boolean("Is currency_id different from the company_currency_id", compute='_compute_same_currency')
|
||||
duplicate_expense_ids = fields.Many2many('hr.expense', compute='_compute_duplicate_expense_ids')
|
||||
|
||||
sample = fields.Boolean()
|
||||
label_total_amount_company = fields.Char(compute='_compute_label_total_amount_company')
|
||||
label_convert_rate = fields.Char(compute='_compute_label_convert_rate')
|
||||
@@ -234,6 +238,35 @@ class HrExpense(models.Model):
|
||||
for expense in self:
|
||||
expense.employee_id = self.env.user.with_company(expense.company_id).employee_id
|
||||
|
||||
@api.depends('employee_id', 'product_id', 'total_amount')
|
||||
def _compute_duplicate_expense_ids(self):
|
||||
self.duplicate_expense_ids = [(5, 0, 0)]
|
||||
|
||||
expenses = self.filtered(lambda e: e.employee_id and e.product_id and e.total_amount)
|
||||
if expenses.ids:
|
||||
duplicates_query = """
|
||||
SELECT ARRAY_AGG(DISTINCT he.id)
|
||||
FROM hr_expense AS he
|
||||
JOIN hr_expense AS ex ON he.employee_id = ex.employee_id
|
||||
AND he.product_id = ex.product_id
|
||||
AND he.date = ex.date
|
||||
AND he.total_amount = ex.total_amount
|
||||
AND he.company_id = ex.company_id
|
||||
AND he.currency_id = ex.currency_id
|
||||
WHERE ex.id in %(expense_ids)s
|
||||
GROUP BY he.employee_id, he.product_id, he.date, he.total_amount, he.company_id, he.currency_id
|
||||
HAVING COUNT(he.id) > 1
|
||||
"""
|
||||
self.env.cr.execute(duplicates_query, {
|
||||
'expense_ids': tuple(expenses.ids),
|
||||
})
|
||||
duplicates = [x[0] for x in self.env.cr.fetchall()]
|
||||
|
||||
for ids in duplicates:
|
||||
exp = expenses.filtered(lambda e: e.id in ids)
|
||||
exp.duplicate_expense_ids = [(6, 0, ids)]
|
||||
expenses = expenses - exp
|
||||
|
||||
@api.onchange('product_id', 'date', 'account_id')
|
||||
def _onchange_product_id_date_account_id(self):
|
||||
rec = self.env['account.analytic.default'].sudo().account_get(
|
||||
@@ -364,7 +397,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
|
||||
sheet = self.env['hr.expense.sheet'].create({
|
||||
'company_id': self.company_id.id,
|
||||
'employee_id': self[0].employee_id.id,
|
||||
'name': todo[0].name if len(todo) == 1 else '',
|
||||
'name': todo[0].name if len(todo) == 1 else _('Expense Report'),
|
||||
'expense_line_ids': [(6, 0, todo.ids)]
|
||||
})
|
||||
return sheet
|
||||
@@ -388,6 +421,14 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
|
||||
res['context'] = {'default_res_model': 'hr.expense', 'default_res_id': self.id}
|
||||
return res
|
||||
|
||||
def action_approve_duplicates(self):
|
||||
root = self.env['ir.model.data'].xmlid_to_res_id("base.partner_root")
|
||||
for expense in self.duplicate_expense_ids:
|
||||
expense.message_post(
|
||||
body=_('%(user)s confirms this expense is not a duplicate with similar expense.', user=self.env.user.name),
|
||||
author_id=root
|
||||
)
|
||||
|
||||
# ----------------------------------------
|
||||
# Business
|
||||
# ----------------------------------------
|
||||
@@ -855,6 +896,7 @@ class HrExpenseSheet(models.Model):
|
||||
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')
|
||||
can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
|
||||
approval_date = fields.Datetime('Approval Date', readonly=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('journal_id_required_posted', "CHECK((state IN ('post', 'done') AND journal_id IS NOT NULL) OR (state NOT IN ('post', 'done')))", 'The journal must be set on posted expense'),
|
||||
@@ -1003,7 +1045,7 @@ class HrExpenseSheet(models.Model):
|
||||
self.write({'state': 'submit'})
|
||||
self.activity_update()
|
||||
|
||||
def approve_expense_sheets(self):
|
||||
def _check_can_approve(self):
|
||||
if not self.user_has_groups('hr_expense.group_hr_expense_team_approver'):
|
||||
raise UserError(_("Only Managers and HR Officers can approve expenses"))
|
||||
elif not self.user_has_groups('hr_expense.group_hr_expense_manager'):
|
||||
@@ -1015,8 +1057,21 @@ class HrExpenseSheet(models.Model):
|
||||
if not self.env.user in current_managers and not self.user_has_groups('hr_expense.group_hr_expense_user') and self.employee_id.expense_manager_id != self.env.user:
|
||||
raise UserError(_("You can only approve your department expenses"))
|
||||
|
||||
def approve_expense_sheets(self):
|
||||
self._check_can_approve()
|
||||
|
||||
duplicates = self.expense_line_ids.duplicate_expense_ids.filtered(lambda exp: exp.state in ['approved', 'done'])
|
||||
if duplicates:
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id('hr_expense.hr_expense_approve_duplicate_action')
|
||||
action['context'] = {'default_sheet_ids': self.ids, 'default_expense_ids': duplicates.ids}
|
||||
return action
|
||||
self._do_approve()
|
||||
|
||||
def _do_approve(self):
|
||||
self._check_can_approve()
|
||||
|
||||
responsible_id = self.user_id.id or self.env.user.id
|
||||
self.write({'state': 'approve', 'user_id': responsible_id})
|
||||
self.write({'state': 'approve', 'user_id': responsible_id, 'approval_date': fields.Datetime.now()})
|
||||
self.activity_update()
|
||||
|
||||
def paid_expense_sheets(self):
|
||||
@@ -1043,7 +1098,7 @@ class HrExpenseSheet(models.Model):
|
||||
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': 'draft'})
|
||||
self.write({'state': 'draft', 'approval_date': False})
|
||||
self.activity_update()
|
||||
return True
|
||||
|
||||
|
||||
@@ -15,3 +15,4 @@ access_account_move_line_user,account.move.line.hr.expense.approver,account.mode
|
||||
access_account_analytic_line_user,account.analytic.line.user,account.model_account_analytic_line,hr_expense.group_hr_expense_team_approver,1,1,1,1
|
||||
access_mail_activity_type_expense_user,mail.activity.type.expense.user,mail.model_mail_activity_type,hr_expense.group_hr_expense_manager,1,1,1,1
|
||||
access_hr_expense_refuse_wizard,access.hr.expense.refuse.wizard,model_hr_expense_refuse_wizard,hr_expense.group_hr_expense_team_approver,1,1,1,0
|
||||
access_hr_expense_approve_duplicate,access.hr.expense.approve.duplicate,model_hr_expense_approve_duplicate,hr_expense.group_hr_expense_team_approver,1,1,1,0
|
||||
|
||||
|
@@ -0,0 +1,65 @@
|
||||
odoo.define('hr_expense.FormView', function (require) {
|
||||
"use strict";
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var FormController = require('web.FormController');
|
||||
var FormView = require('web.FormView');
|
||||
|
||||
var viewRegistry = require('web.view_registry');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
var ExpenseFormController = FormController.extend({
|
||||
_onButtonClicked: function (ev) {
|
||||
if (ev.data.attrs.name === "action_submit_expenses") {
|
||||
ev.stopPropagation();
|
||||
var record = this.model.get(this.handle);
|
||||
if (record.data.duplicate_expense_ids.count) {
|
||||
var _super = this._super.bind(this, ev);
|
||||
const recordID = record.data.id;
|
||||
this._showConfirmDialog(_super).then(() => {
|
||||
return this._rpc({
|
||||
model: 'hr.expense',
|
||||
method: 'action_approve_duplicates',
|
||||
args: [recordID],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
_showConfirmDialog: function(confirm_callback) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
Dialog.confirm(this, _t("An expense of same category, amount and date already exists."), {
|
||||
buttons: [
|
||||
{
|
||||
text: _t("Save Anyways"),
|
||||
classes: 'btn-primary',
|
||||
close: true,
|
||||
click: function() {
|
||||
confirm_callback();
|
||||
resolve();
|
||||
}
|
||||
}, {
|
||||
text: _t("Cancel"),
|
||||
close: true,
|
||||
click: reject,
|
||||
}
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var ExpenseFormView = FormView.extend({
|
||||
config: _.extend({}, FormView.prototype.config, {
|
||||
Controller: ExpenseFormController,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('hr_expense_form_view', ExpenseFormView);
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
<template id="assets_backend" name="HR Expense Assets assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/hr_expense/static/src/js/expense_views.js"/>
|
||||
<script type="text/javascript" src="/hr_expense/static/src/js/expense_form_view.js"/>
|
||||
<script type="text/javascript" src="/hr_expense/static/src/js/expense_qr_code_action.js"/>
|
||||
<script type="text/javascript" src="/hr_expense/static/src/js/upload_mixin.js"/>
|
||||
</xpath>
|
||||
|
||||
@@ -108,12 +108,12 @@
|
||||
<field name="name">hr.expense.view.form</field>
|
||||
<field name="model">hr.expense</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Expenses" class="o_expense_form">
|
||||
<form string="Expenses" class="o_expense_form" js_class="hr_expense_form_view">
|
||||
<header>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '<=', 0), ('sheet_id', '!=', False)]}"/>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', '|', ('id', '=', False), ('attachment_number', '<=', 0), ('sheet_id', '!=', False)]}"/>
|
||||
<widget name="attach_document" string="Attach Receipt" action="message_post" attrs="{'invisible': ['|', ('attachment_number', '<', 1), ('id','=',False)]}"/>
|
||||
<widget name="attach_document" string="Attach Receipt" action="message_post" highlight="1" attrs="{'invisible': ['|',('attachment_number', '>=', 1), ('id','=',False)]}"/>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '>=', 1), ('sheet_id', '!=', False)]}"/>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="o_expense_submit" attrs="{'invisible': ['|', '|', ('id', '=', False), ('attachment_number', '>=', 1), ('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': [('sheet_id', '=', False)]}"/>
|
||||
</header>
|
||||
@@ -174,6 +174,7 @@
|
||||
<field name="analytic_account_id" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]" groups="analytic.group_analytic_accounting" attrs="{'readonly': [('is_editable', '=', False)]}"/>
|
||||
<field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_tags" attrs="{'readonly': [('is_editable', '=', False)]}"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="duplicate_expense_ids" invisible="1" />
|
||||
</group><group attrs="{'invisible': [('product_has_cost', '=', True)]}">
|
||||
<label for="payment_mode"/>
|
||||
<div>
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
from . import hr_expense_refuse_reason
|
||||
from . import account_payment_register
|
||||
from . import hr_expense_approve_duplicate
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class HrExpenseApproveDuplicate(models.TransientModel):
|
||||
"""
|
||||
This wizard is shown whenever an approved expense is similar to one being
|
||||
approved. The user has the opportunity to still validate it or decline.
|
||||
"""
|
||||
|
||||
_name = "hr.expense.approve.duplicate"
|
||||
_description = "Expense Approve Duplicate"
|
||||
|
||||
sheet_ids = fields.Many2many('hr.expense.sheet')
|
||||
expense_ids = fields.Many2many('hr.expense', readonly=True)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
|
||||
if 'sheet_ids' in fields:
|
||||
res['sheet_ids'] = [(6, 0, self.env.context.get('default_sheet_ids', []))]
|
||||
if 'duplicate_expense_ids' in fields:
|
||||
res['expense_ids'] = [(6, 0, self.env.context.get('default_expense_ids', []))]
|
||||
|
||||
return res
|
||||
|
||||
def action_approve(self):
|
||||
self.sheet_ids._do_approve()
|
||||
|
||||
def action_refuse(self):
|
||||
self.sheet_ids.refuse_sheet(_('Duplicate Expense'))
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_expense_approve_duplicate_view_form" model="ir.ui.view">
|
||||
<field name="model">hr.expense.approve.duplicate</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Expense Validate Duplicate">
|
||||
<field name="sheet_ids" invisible="1" />
|
||||
<p>The following approved expenses have similar employee, amount and category than some expenses of this report. Please verify this report does not contain duplicates.</p>
|
||||
<field name="expense_ids" nolabel="1">
|
||||
<tree>
|
||||
<field name="date" readonly="1" />
|
||||
<field name="employee_id" readonly="1" />
|
||||
<field name="product_id" readonly="1" />
|
||||
<field name="total_amount_company" readonly="1" />
|
||||
<field name="name" readonly="1" />
|
||||
<field name="approved_by" readonly="1" />
|
||||
<field name="approved_on" readonly="1" />
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button string="Refuse" class="btn-primary" name="action_refuse" type="object" attrs="{'invisible': [('sheet_ids', '=', [])]}" />
|
||||
<button string="Approve" class="btn-secondary" name="action_approve" type="object" attrs="{'invisible': [('sheet_ids', '=', [])]}" />
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_expense_approve_duplicate_action" model="ir.actions.act_window">
|
||||
<field name="name">Validate Duplicate Expenses</field>
|
||||
<field name="res_model">hr.expense.approve.duplicate</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="hr_expense_approve_duplicate_view_form"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user