From 40a82e1fe48c046db650a79ca62055590b31cf2c Mon Sep 17 00:00:00 2001 From: sofiagvaladze Date: Thu, 19 May 2022 09:37:14 +0000 Subject: [PATCH] [IMP] hr_expense,sale_expense: UX improvements Major changes: - converted list view to owl - merged menus - added CREATE REPORT button on hr.expense that reports either ticked draft expenses, or all the draft expenses for the user - added dynamic buttons on expense.sheet list view - added searchpanel on expense.sheet for team approvers and above - moved (and changed) expense categories from demo to data - added support on drag'n'drop while in list and kanban view of expense for quick upload task - 2831036 closes odoo/odoo#93802 Related: odoo/enterprise#28474 Related: odoo/upgrade#3778 Signed-off-by: Kevin Baptiste --- addons/hr_expense/__manifest__.py | 5 +- addons/hr_expense/data/hr_expense_data.xml | 71 ++++- addons/hr_expense/data/hr_expense_demo.xml | 133 ++------- addons/hr_expense/models/hr_expense.py | 31 +- .../expense_dashboard.js | 9 +- .../src/components/expense_dashboard.xml | 19 ++ .../static/src/js/expense_qr_code_action.js | 18 -- .../hr_expense/static/src/js/expense_views.js | 137 --------- .../hr_expense/static/src/js/upload_mixin.js | 76 ----- .../static/src/mixins/document_upload.js | 60 +++- .../static/src/scss/hr_expense.scss | 16 + addons/hr_expense/static/src/views/kanban.js | 10 +- addons/hr_expense/static/src/views/kanban.xml | 20 +- addons/hr_expense/static/src/views/list.js | 94 ++++++ addons/hr_expense/static/src/views/list.xml | 57 ++++ .../static/src/xml/documents_upload_views.xml | 13 + addons/hr_expense/tests/test_expenses.py | 2 +- addons/hr_expense/views/hr_expense_views.xml | 278 ++++++------------ addons/sale_expense/__manifest__.py | 2 +- ...expense_demo.xml => sale_expense_data.xml} | 8 +- .../sale_expense/models/product_template.py | 23 +- .../sale_expense/views/hr_expense_views.xml | 2 +- addons/sale_expense/views/product_view.xml | 19 +- 23 files changed, 525 insertions(+), 578 deletions(-) rename addons/hr_expense/static/src/{mixins => components}/expense_dashboard.js (81%) create mode 100644 addons/hr_expense/static/src/components/expense_dashboard.xml delete mode 100644 addons/hr_expense/static/src/js/expense_qr_code_action.js delete mode 100644 addons/hr_expense/static/src/js/expense_views.js delete mode 100644 addons/hr_expense/static/src/js/upload_mixin.js create mode 100644 addons/hr_expense/static/src/views/list.js create mode 100644 addons/hr_expense/static/src/views/list.xml rename addons/sale_expense/data/{sale_expense_demo.xml => sale_expense_data.xml} (63%) diff --git a/addons/hr_expense/__manifest__.py b/addons/hr_expense/__manifest__.py index dc357795505..0504030081b 100644 --- a/addons/hr_expense/__manifest__.py +++ b/addons/hr_expense/__manifest__.py @@ -54,11 +54,9 @@ This module also uses analytic accounting and is compatible with the invoice on 'application': True, 'assets': { 'web.assets_backend': [ + 'hr_expense/static/src/components/*.js', 'hr_expense/static/src/mixins/*.js', 'hr_expense/static/src/views/*.js', - 'hr_expense/static/src/js/expense_views.js', - 'hr_expense/static/src/js/expense_qr_code_action.js', - 'hr_expense/static/src/js/upload_mixin.js', 'hr_expense/static/src/scss/hr_expense.scss', ], 'web.assets_tests': [ @@ -66,6 +64,7 @@ This module also uses analytic accounting and is compatible with the invoice on 'hr_expense/static/tests/tours/expense_upload_tours.js', ], 'web.assets_qweb': [ + 'hr_expense/static/src/components/*.xml', 'hr_expense/static/src/views/*.xml', 'hr_expense/static/src/xml/**/*', ], diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml index 4ff52f6be77..52326d5701e 100644 --- a/addons/hr_expense/data/hr_expense_data.xml +++ b/addons/hr_expense/data/hr_expense_data.xml @@ -1,22 +1,75 @@ - - Expenses (Fixed Cost) - 0.0 - 1.0 - service - + + + Meals + Restaurants, business lunches, etc. + 0.0 + + + FOOD + + + - - Expenses - 0.0 + + Travel & Accommodation + Hotel, plane ticket, taxi, etc. + 0.0 + service + + + TRANS & ACC + + + + + + + Mileage + 1.0 + + + MIL + + + + + + + Gifts + Gifts to customers or vendors + 0.0 + + + GIFT + + + + + + + Communication + Phone bills, postage, etc. + 0.0 + + + COMM + + + + + + + Others 0.0 service EXP_GEN + diff --git a/addons/hr_expense/data/hr_expense_demo.xml b/addons/hr_expense/data/hr_expense_demo.xml index 32628852bdc..065520d2ab1 100644 --- a/addons/hr_expense/data/hr_expense_demo.xml +++ b/addons/hr_expense/data/hr_expense_demo.xml @@ -33,87 +33,15 @@ - - Food & beverages - 10 - 1 - - - FOOD - - - - - - - Flights, train, bus, taxi, parking - 1 - service - - - TRANS - - - - - - - Mileage - 0.37 - - - MIL - - - - - - - Accomodation - 1 - service - - - ACC - - - - - - - Daily Allowance - 100 - service - - - DAL - - - - - - - 1 - Other expenses - - - OTHER - - - - - - Screen - - - - + + + @@ -121,17 +49,16 @@ Laptop - - - - + + + Travel by car - + @@ -140,7 +67,7 @@ BreakFast - + @@ -155,7 +82,7 @@ Travel by Air - + @@ -167,7 +94,7 @@ Hotel Expenses - + @@ -179,7 +106,7 @@ Lunch with Customer - + @@ -190,7 +117,7 @@ Lunch - + @@ -208,7 +135,7 @@ Travel by Car - + @@ -220,10 +147,10 @@ Lunch with Customer - + - - + + @@ -238,7 +165,7 @@ Pizzas - + @@ -249,7 +176,7 @@ Drinks - + @@ -260,10 +187,10 @@ Paintball - + - - + + @@ -278,10 +205,10 @@ Chairs - + - - + + @@ -289,10 +216,10 @@ Lamp - + - - + + @@ -301,10 +228,10 @@ Car tyres - + - - + + diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index ce7675a5191..3487c9c7b30 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -55,7 +55,7 @@ class HrExpense(models.Model): states={'approved': [('readonly', True)], 'done': [('readonly', True)]}, default=_default_employee_id, domain=lambda self: self._get_employee_id_domain(), check_company=True) # product_id not required to allow create an expense without product via mail alias, but should be required on the view. - product_id = fields.Many2one('product.product', string='Product', tracking=True, states={'done': [('readonly', True)]}, domain="[('can_be_expensed', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", ondelete='restrict') + product_id = fields.Many2one('product.product', string='Category', tracking=True, states={'done': [('readonly', True)]}, domain="[('can_be_expensed', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", ondelete='restrict') product_description = fields.Html(compute='_compute_product_description') product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', compute='_compute_from_product_id_company_id', store=True, precompute=True, copy=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, @@ -82,7 +82,7 @@ class HrExpense(models.Model): analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") account_id = fields.Many2one('account.account', compute='_compute_from_product_id_company_id', store=True, readonly=False, precompute=True, string='Account', domain="[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card')), ('company_id', '=', company_id)]", help="An expense account is expected") - description = fields.Text('Notes...', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}) + description = fields.Text('Internal Notes', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}) payment_mode = fields.Selection([ ("own_account", "Employee (to reimburse)"), ("company_account", "Company") @@ -472,6 +472,18 @@ Or send your receipts at + + +
+ + + +
+ + +
+
+ +
+
+
+
+
+ diff --git a/addons/hr_expense/static/src/js/expense_qr_code_action.js b/addons/hr_expense/static/src/js/expense_qr_code_action.js deleted file mode 100644 index 7ea4dcbafa2..00000000000 --- a/addons/hr_expense/static/src/js/expense_qr_code_action.js +++ /dev/null @@ -1,18 +0,0 @@ -odoo.define('hr_expense.qr_code_action', function (require) { -"use strict"; - -const AbstractAction = require('web.AbstractAction'); -const core = require('web.core'); - -const QRModalAction = AbstractAction.extend({ - template: 'hr_expense_qr_code', - xmlDependencies: ['/hr_expense/static/src/xml/expense_qr_modal_template.xml'], - - init: function(parent, action){ - this._super.apply(this, arguments); - this.url = _.str.sprintf("/report/barcode/?barcode_type=QR&value=%s&width=256&height=256&humanreadable=1", action.params.url); - }, -}); - -core.action_registry.add('expense_qr_code_modal', QRModalAction); -}); diff --git a/addons/hr_expense/static/src/js/expense_views.js b/addons/hr_expense/static/src/js/expense_views.js deleted file mode 100644 index 043edae5921..00000000000 --- a/addons/hr_expense/static/src/js/expense_views.js +++ /dev/null @@ -1,137 +0,0 @@ -odoo.define('hr_expense.expenses.tree', function (require) { -"use strict"; - var DocumentUploadMixin = require('hr_expense.documents.upload.mixin'); - var ListController = require('web.ListController'); - var ListView = require('web.ListView'); - var viewRegistry = require('web.view_registry'); - var core = require('web.core'); - var ListRenderer = require('web.ListRenderer'); - var session = require('web.session'); - const config = require('web.config'); - - var QWeb = core.qweb; - - var ExpensesListController = ListController.extend(DocumentUploadMixin, { - buttons_template: 'ExpensesListView.buttons', - events: _.extend({}, ListController.prototype.events, { - 'click .o_button_upload_expense': '_onUpload', - 'change .o_expense_documents_upload .o_form_binary_form': '_onAddAttachment', - }), - /** - * @override - */ - init: function () { - this._super.apply(this, arguments); - this.isMobile = config.device.isMobileDevice; - }, - }); - - const ExpenseQRCodeMixin = { - async _renderView() { - const self = this; - await this._super(...arguments); - const google_url = "https://play.google.com/store/apps/details?id=com.odoo.mobile"; - const apple_url = "https://apps.apple.com/be/app/odoo/id1272543640"; - const action_desktop = { - name: 'Download our App', - type: 'ir.actions.client', - tag: 'expense_qr_code_modal', - params: {'url': "https://apps.apple.com/be/app/odoo/id1272543640"}, - target: 'new', - }; - this.$el.find('img.o_expense_apple_store').on('click', function(event) { - event.preventDefault(); - if (!config.device.isMobileDevice) { - self.do_action(_.extend(action_desktop, {params: {'url': apple_url}})); - } else { - self.do_action({type: 'ir.actions.act_url', url: apple_url}); - } - }); - this.$el.find('img.o_expense_google_store').on('click', function(event) { - event.preventDefault(); - if (!config.device.isMobileDevice) { - self.do_action(_.extend(action_desktop, {params: {'url': google_url}})); - } else { - self.do_action({type: 'ir.actions.act_url', url: google_url}); - } - }); - }, - }; - - const ExpenseDashboardMixin = { - _render: async function () { - var self = this; - await this._super(...arguments); - const result = await this._rpc({ - model: 'hr.expense', - method: 'get_expense_dashboard', - context: this.context, - }); - - self.$el.parent().find('.o_expense_container').remove(); - const elem = QWeb.render('hr_expense.dashboard_list_header', { - expenses: result, - render_monetary_field: self.render_monetary_field, - }); - self.$el.before(elem); - }, - render_monetary_field: function (value, currency_id) { - value = value.toFixed(2); - var currency = session.get_currency(currency_id); - if (currency) { - if (currency.position === "after") { - value += currency.symbol; - } else { - value = currency.symbol + value; - } - } - return value; - } - }; - - // Expense List Renderer - var ExpenseListRenderer = ListRenderer.extend(ExpenseQRCodeMixin); - - // Expense List Renderer with the Header - // Used in "My Expenses to Report", "All My Expenses" & "My Reports" - var ExpenseListRendererHeader = ExpenseListRenderer.extend(ExpenseDashboardMixin); - - var ExpensesListViewDashboardUpload = ListView.extend({ - config: _.extend({}, ListView.prototype.config, { - Renderer: ExpenseListRenderer, - Controller: ExpensesListController, - }), - }); - - // Used in "My Expenses to Report" & "All My Expenses" - var ExpensesListViewDashboardUploadHeader = ExpensesListViewDashboardUpload.extend({ - config: _.extend({}, ExpensesListViewDashboardUpload.prototype.config, { - Renderer: ExpenseListRendererHeader, - }), - }); - - // The dashboard view of the expense module - var ExpensesListViewDashboard = ListView.extend({ - config: _.extend({}, ListView.prototype.config, { - Renderer: ExpenseListRenderer, - Controller: ExpensesListController, - }), - }); - - // The dashboard view of the expense module with an header - // Used in "My Expenses" - var ExpensesListViewDashboardHeader = ExpensesListViewDashboard.extend({ - config: _.extend({}, ExpensesListViewDashboard.prototype.config, { - Renderer: ExpenseListRendererHeader, - }) - }); - - viewRegistry.add('hr_expense_tree_dashboard_upload', ExpensesListViewDashboardUpload); - // Tree view with the header. - // Used in "My Expenses to Report" & "All My Expenses" - viewRegistry.add('hr_expense_tree_dashboard_upload_header', ExpensesListViewDashboardUploadHeader); - viewRegistry.add('hr_expense_tree_dashboard', ExpensesListViewDashboard); - // Tree view with the header. - // Used in "My Reports" - viewRegistry.add('hr_expense_tree_dashboard_header', ExpensesListViewDashboardHeader); -}); diff --git a/addons/hr_expense/static/src/js/upload_mixin.js b/addons/hr_expense/static/src/js/upload_mixin.js deleted file mode 100644 index b680b670d35..00000000000 --- a/addons/hr_expense/static/src/js/upload_mixin.js +++ /dev/null @@ -1,76 +0,0 @@ -odoo.define('hr_expense.documents.upload.mixin', function (require) { -"use strict"; - -var core = require('web.core'); -var config = require('web.config'); -var _t = core._t; -var qweb = core.qweb; - -/** -* Mixin for uploading single or multiple documents. -*/ -var DocumentUploadMixin = { - start: function () { - // define a unique uploadId and a callback method - this.fileUploadID = _.uniqueId('hr_expense_document_upload'); - $(window).on(this.fileUploadID, this._onFileUploaded.bind(this)); - return this._super.apply(this, arguments); - }, - /** - * @private - */ - _onAddAttachment: function (ev) { - // Auto submit form once we've selected an attachment - var $input = $(ev.currentTarget).find('input.o_input_file'); - if ($input.val() !== '') { - var $binaryForm = this.$('.o_expense_documents_upload form.o_form_binary_form'); - $binaryForm.submit(); - } - }, - /** - * @private - */ - _onFileUploaded: function () { - // Callback once attachment have been created, create an expense with attachment ids - var self = this; - var attachments = Array.prototype.slice.call(arguments, 1); - // Get id from result - var attachent_ids = attachments.reduce(function(filtered, record) { - if (record.id) { - filtered.push(record.id); - } - return filtered; - }, []); - if (!attachent_ids.length) { - return self.displayNotification({ message: _t("An error occurred during the upload") }); - } - var myContext = this.initialState.context - myContext['isMobile'] = config.device.isMobile - return this._rpc({ - model: 'hr.expense', - method: 'create_expense_from_attachments', - args: ["", attachent_ids, this.viewType], - context: myContext, - }).then(function(result) { - self.do_action(result); - }); - }, - /** - * @private - * @param {Event} event - */ - _onUpload: function (event) { - // If hidden upload form don't exists, create it - var $formContainer = this.$('.o_content').find('.o_expense_documents_upload'); - if (!$formContainer.length) { - $formContainer = $(qweb.render('hr.expense.DocumentsHiddenUploadForm', {widget: this})); - $formContainer.appendTo(this.$('.o_content')); - } - // Trigger the input to select a file - this.$('.o_expense_documents_upload .o_input_file').click(); - }, -}; - -return DocumentUploadMixin; - -}); diff --git a/addons/hr_expense/static/src/mixins/document_upload.js b/addons/hr_expense/static/src/mixins/document_upload.js index 9bc62313c1c..bbcf300bc64 100644 --- a/addons/hr_expense/static/src/mixins/document_upload.js +++ b/addons/hr_expense/static/src/mixins/document_upload.js @@ -1,8 +1,57 @@ /** @odoo-module */ -import { useService } from '@web/core/utils/hooks'; +import { useBus, useService } from '@web/core/utils/hooks'; -const { useRef } = owl; +const { useRef, useEffect, useState } = owl; + +export const ExpenseDocumentDropZone = { + setup() { + this._super(); + this.dragState = useState({ + showDragZone: false, + }); + this.root = useRef("root"); + + useEffect( + (el) => { + if (!el) { + return; + } + const highlight = this.highlight.bind(this); + const unhighlight = this.unhighlight.bind(this); + const drop = this.onDrop.bind(this); + el.addEventListener("dragover", highlight); + el.addEventListener("dragleave", unhighlight); + el.addEventListener("drop", drop); + return () => { + el.removeEventListener("dragover", highlight); + el.removeEventListener("dragleave", unhighlight); + el.removeEventListener("drop", drop); + }; + }, + () => [document.querySelector('.o_content')] + ); + }, + + highlight(ev) { + ev.stopPropagation(); + ev.preventDefault(); + this.dragState.showDragZone = true; + }, + + unhighlight(ev) { + ev.stopPropagation(); + ev.preventDefault(); + this.dragState.showDragZone = false; + }, + + async onDrop(ev) { + ev.preventDefault(); + await this.env.bus.trigger("change_file_input", { + files: ev.dataTransfer.files, + }); + }, +}; export const ExpenseDocumentUpload = { setup() { @@ -11,8 +60,13 @@ export const ExpenseDocumentUpload = { this.notification = useService('notification'); this.orm = useService('orm'); this.http = useService('http'); - this.fileInput = useRef('fileInput'); + this.root = useRef("root"); + + useBus(this.env.bus, "change_file_input", async (ev) => { + this.fileInput.el.files = ev.detail.files; + await this.onChangeFileInput(); + }); }, uploadDocument() { diff --git a/addons/hr_expense/static/src/scss/hr_expense.scss b/addons/hr_expense/static/src/scss/hr_expense.scss index da4481e753f..a928874831d 100644 --- a/addons/hr_expense/static/src/scss/hr_expense.scss +++ b/addons/hr_expense/static/src/scss/hr_expense.scss @@ -18,3 +18,19 @@ } } } + +.o_dropzone { + width: 100%; + height: 100%; + position: absolute; + background-color: #AAAA; + z-index: 2; + left: 0; + top: 0; + i { + justify-content: center; + display: flex; + align-items: center; + height: 100%; + } +} diff --git a/addons/hr_expense/static/src/views/kanban.js b/addons/hr_expense/static/src/views/kanban.js index e76f0afcdb4..8b56bcbc758 100644 --- a/addons/hr_expense/static/src/views/kanban.js +++ b/addons/hr_expense/static/src/views/kanban.js @@ -3,9 +3,9 @@ import { registry } from '@web/core/registry'; import { patch } from '@web/core/utils/patch'; -import { ExpenseDashboardMixin } from '../mixins/expense_dashboard'; +import { ExpenseDashboard } from '../components/expense_dashboard'; import { ExpenseMobileQRCode } from '../mixins/qrcode'; -import { ExpenseDocumentUpload } from '../mixins/document_upload'; +import { ExpenseDocumentUpload, ExpenseDocumentDropZone } from '../mixins/document_upload'; import { kanbanView } from '@web/views/kanban/kanban_view'; import { KanbanController } from '@web/views/kanban/kanban_controller'; @@ -16,10 +16,12 @@ patch(ExpenseKanbanController.prototype, 'expense_kanban_controller_upload', Exp export class ExpenseKanbanRenderer extends KanbanRenderer {} patch(ExpenseKanbanRenderer.prototype, 'expense_kanban_renderer_qrcode', ExpenseMobileQRCode); +patch(ExpenseKanbanRenderer.prototype, 'expense_kanban_renderer_qrcode_dzone', ExpenseDocumentDropZone); +ExpenseKanbanRenderer.template = 'hr_expense.KanbanRenderer'; export class ExpenseDashboardKanbanRenderer extends ExpenseKanbanRenderer {} -patch(ExpenseDashboardKanbanRenderer.prototype, 'expense_kanban_renderer_dashboard', ExpenseDashboardMixin); -ExpenseDashboardKanbanRenderer.template = 'hr_expense.KanbanRenderer'; +ExpenseDashboardKanbanRenderer.components = { ...ExpenseDashboardKanbanRenderer.components, ExpenseDashboard}; +ExpenseDashboardKanbanRenderer.template = 'hr_expense.DashboardKanbanRenderer'; registry.category('views').add('hr_expense_kanban', { ...kanbanView, diff --git a/addons/hr_expense/static/src/views/kanban.xml b/addons/hr_expense/static/src/views/kanban.xml index 28adb7e02df..58d88c967be 100644 --- a/addons/hr_expense/static/src/views/kanban.xml +++ b/addons/hr_expense/static/src/views/kanban.xml @@ -2,22 +2,18 @@ -
- - - -
- - -
-
- -
-
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + diff --git a/addons/hr_expense/static/src/xml/documents_upload_views.xml b/addons/hr_expense/static/src/xml/documents_upload_views.xml index af7771eeccc..33b05c53973 100644 --- a/addons/hr_expense/static/src/xml/documents_upload_views.xml +++ b/addons/hr_expense/static/src/xml/documents_upload_views.xml @@ -13,6 +13,12 @@
+ +
+ +
+
+ + + + + +
-
@@ -284,7 +281,7 @@ - + hr.expense.kanban hr.expense @@ -314,9 +311,13 @@ hr.expense + + - + + + @@ -382,7 +383,7 @@ Expenses Analysis hr.expense - graph,pivot,tree,kanban,form + graph,pivot,tree,form

@@ -411,57 +412,20 @@ - - kanban - - - - - - My Expenses to Report - hr.expense - tree,kanban,form,graph,pivot,activity - - {'search_default_my_expenses': 1, 'search_default_no_report': 1} - -

-

- Did you try the mobile app? -

-

-

Snap pictures of your receipts and let Odoo
automatically create expenses for you.

-

- - Apple App Store - - - Google Play Store - -

- - - - - - tree - - - - - - kanban - - - - - All My Expenses + My Expenses hr.expense tree,kanban,form,graph,pivot,activity {'search_default_my_expenses': 1}

+

+ Drag and drop files to create expenses +

+

+ Or +

Did you try the mobile app?

@@ -520,7 +484,7 @@ product.product.expense.form product.product -
+ @@ -561,7 +525,7 @@ - + @@ -585,30 +549,46 @@ + + product.product.expense.categories.tree.view + product.product + + + + + + + + + + + + - Expense Products + Expense Categories product.product - kanban,tree,form + tree,kanban,form {"default_can_be_expensed": 1, 'default_detailed_type': 'service'} [('can_be_expensed', '=', True)]

- No expense products found. Let's create one! + No expense categories found. Let's create one!

- Expense products can be reinvoiced to your customers. + Expense categories can be reinvoiced to your customers.

- + tree + - + kanban @@ -624,21 +604,8 @@ hr.expense.sheet.tree hr.expense.sheet - -
-
+ + @@ -655,19 +622,6 @@ - - hr.expense.sheet.dashboard.tree - hr.expense.sheet - 20 - - primary - - - hr_expense_tree_dashboard - - - - hr.expense.sheet.dashboard.tree @@ -677,7 +631,7 @@ primary - hr_expense_tree_dashboard_header + hr_expense_dashboard_tree @@ -772,25 +726,33 @@ - + + + +