[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 <kba@odoo.com>
This commit is contained in:
sofiagvaladze
2022-08-27 03:32:54 +02:00
parent edfcb5e626
commit 40a82e1fe4
23 changed files with 525 additions and 578 deletions
+2 -3
View File
@@ -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/**/*',
],
+62 -9
View File
@@ -1,22 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="product_product_fixed_cost" model="product.product">
<field name="name">Expenses (Fixed Cost)</field>
<field name="list_price">0.0</field>
<field name="standard_price">1.0</field>
<field name="type">service</field>
<field name="categ_id" ref="product.cat_expense"/>
<!-- sgv note - icons will be replaced. Design task is ongoing -->
<record id="expense_product_meal" model="product.product">
<field name="name">Meals</field>
<field name="description">Restaurants, business lunches, etc.</field>
<field name="standard_price">0.0</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">FOOD</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/food.png"/>
</record>
<record id="product_product_zero_cost" model="product.product">
<field name="name">Expenses</field>
<field name="list_price">0.0</field>
<record id="expense_product_travel_accommodation" model="product.product">
<field name="name">Travel &amp; Accommodation</field>
<field name="description">Hotel, plane ticket, taxi, etc.</field>
<field name="standard_price">0.0</field>
<field name="type">service</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">TRANS &amp; ACC</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/transport.png"/>
</record>
<record id="expense_product_mileage" model="product.product">
<field name="name">Mileage</field>
<field name="standard_price">1.0</field>
<field name="uom_id" ref="uom.product_uom_km"/>
<field name="uom_po_id" ref="uom.product_uom_km"/>
<field name="default_code">MIL</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/mileage.png"/>
</record>
<record id="expense_product_gift" model="product.product">
<field name="name">Gifts</field>
<field name="description">Gifts to customers or vendors</field>
<field name="standard_price">0.0</field>
<field name="uom_id" ref="uom.product_uom_km"/>
<field name="uom_po_id" ref="uom.product_uom_km"/>
<field name="default_code">GIFT</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/allowance.png"/>
</record>
<record id="expense_product_communication" model="product.product">
<field name="name">Communication</field>
<field name="description">Phone bills, postage, etc.</field>
<field name="standard_price">0.0</field>
<field name="uom_id" ref="uom.product_uom_km"/>
<field name="uom_po_id" ref="uom.product_uom_km"/>
<field name="default_code">COMM</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/allowance.png"/>
</record>
<record id="product_product_no_cost" model="product.product">
<field name="name">Others</field>
<field name="standard_price">0.0</field>
<field name="type">service</field>
<field name="default_code">EXP_GEN</field>
<field name="categ_id" ref="product.cat_expense"/>
<field name="can_be_expensed" eval="True"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/other.png"/>
</record>
</data>
<data>
+30 -103
View File
@@ -33,87 +33,15 @@
<field name="address_home_id" ref="res_partner_address_fp"/>
</record>
<record id="food_expense_product" model="product.product">
<field name="name">Food &amp; beverages</field>
<field name="list_price">10</field>
<field name="standard_price">1</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">FOOD</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/food.png"/>
</record>
<record id="trans_expense_product" model="product.product">
<field name="name">Flights, train, bus, taxi, parking</field>
<field name="standard_price">1</field>
<field name="type">service</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">TRANS</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/transport.png"/>
</record>
<record id="mileage_expense_product" model="product.product">
<field name="name">Mileage</field>
<field name="standard_price">0.37</field>
<field name="uom_id" ref="uom.product_uom_km"/>
<field name="uom_po_id" ref="uom.product_uom_km"/>
<field name="default_code">MIL</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/mileage.png"/>
</record>
<record id="accomodation_expense_product" model="product.product">
<field name="name">Accomodation</field>
<field name="standard_price">1</field>
<field name="type">service</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">ACC</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/accomodation.png"/>
</record>
<record id="allowance_expense_product" model="product.product">
<field name="name">Daily Allowance</field>
<field name="standard_price">100</field>
<field name="type">service</field>
<field name="uom_id" ref="uom.product_uom_day"/>
<field name="uom_po_id" ref="uom.product_uom_day"/>
<field name="default_code">DAL</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/allowance.png"/>
</record>
<record id="other_expense_product" model="product.product">
<field name="standard_price">1</field>
<field name="name">Other expenses</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">OTHER</field>
<field name="can_be_expensed" eval="True"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="image_1920" type="base64" file="hr_expense/static/img/other.png"/>
</record>
<!-- ++++++++++++++ Expense sheet for Admin ++++++++++++++-->
<record id="screen_expense" model="hr.expense">
<field name="name">Screen</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="product_id" ref="other_expense_product"/>
<field eval="289.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field eval="1.0" name="quantity"/>
<field name="product_id" ref="product_product_no_cost"/>
<field eval="0.0" name="unit_amount"/>
<field eval="289.0" name="total_amount"/>
<field name="date" eval="time.strftime('%Y')+'-04-03'"/>
</record>
@@ -121,17 +49,16 @@
<field name="name">Laptop</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="product_id" ref="other_expense_product"/>
<field eval="889.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field eval="1.0" name="quantity"/>
<field name="product_id" ref="product_product_no_cost"/>
<field eval="0.0" name="unit_amount"/>
<field eval="889.0" name="total_amount"/>
<field name="date" eval="time.strftime('%Y')+'-04-03'"/>
</record>
<record id="travel_admin_by_car_expense" model="hr.expense">
<field name="name">Travel by car</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="product_id" ref="mileage_expense_product"/>
<field name="product_id" ref="expense_product_mileage"/>
<field eval="0.52" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_km"/>
<field eval="320.0" name="quantity"/>
@@ -140,7 +67,7 @@
<record id="breakfast_admin_expense" model="hr.expense">
<field name="name">BreakFast</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="product_id" ref="food_expense_product"/>
<field name="product_id" ref="expense_product_meal"/>
<field eval="20" name="unit_amount"/>
<field eval="1.0" name="quantity"/>
</record>
@@ -155,7 +82,7 @@
<field name="name">Travel by Air</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="product_id" ref="trans_expense_product"/>
<field name="product_id" ref="expense_product_travel_accommodation"/>
<field eval="700.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field eval="1.0" name="quantity"/>
@@ -167,7 +94,7 @@
<field name="name">Hotel Expenses</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="accomodation_expense_product"/>
<field name="product_id" ref="expense_product_travel_accommodation"/>
<field eval="400.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field eval="5.0" name="quantity"/>
@@ -179,7 +106,7 @@
<field name="name">Lunch with Customer</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="food_expense_product"/>
<field name="product_id" ref="expense_product_meal"/>
<field eval="152.8" name="unit_amount"/>
<field name="date" eval="time.strftime('%Y-%m')+'-13'"/>
<field eval="1.0" name="quantity"/>
@@ -190,7 +117,7 @@
<field name="name">Lunch</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="food_expense_product"/>
<field name="product_id" ref="expense_product_meal"/>
<field name="date" eval="time.strftime('%Y-%m')+'-15'"/>
<field eval="56.8" name="unit_amount"/>
<field eval="1.0" name="quantity"/>
@@ -208,7 +135,7 @@
<field name="name">Travel by Car</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="product_id" ref="mileage_expense_product"/>
<field name="product_id" ref="expense_product_mileage"/>
<field eval="0.52" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_km"/>
<field name="date" eval="time.strftime('%Y')+'-01-15'"/>
@@ -220,10 +147,10 @@
<field name="name">Lunch with Customer</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="other_expense_product"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-01-15'"/>
<field eval="152.8" name="unit_amount"/>
<field eval="1.0" name="quantity"/>
<field eval="0.0" name="unit_amount"/>
<field eval="152.8" name="total_amount"/>
<field name="sheet_id" ref="customer_meeting_sheet"/>
</record>
@@ -238,7 +165,7 @@
<field name="name">Pizzas</field>
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="food_expense_product"/>
<field name="product_id" ref="expense_product_meal"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="12.5" name="unit_amount"/>
<field eval="12.0" name="quantity"/>
@@ -249,7 +176,7 @@
<field name="name">Drinks</field>
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="food_expense_product"/>
<field name="product_id" ref="expense_product_meal"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="2.5" name="unit_amount"/>
<field eval="17.0" name="quantity"/>
@@ -260,10 +187,10 @@
<field name="name">Paintball</field>
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="other_expense_product"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="25" name="unit_amount"/>
<field eval="12.0" name="quantity"/>
<field eval="0.0" name="unit_amount"/>
<field eval="25" name="total_amount"/>
<field name="sheet_id" ref="team_building_sheet"/>
</record>
@@ -278,10 +205,10 @@
<field name="name">Chairs</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="other_expense_product"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
<field eval="55.75" name="unit_amount"/>
<field eval="6.0" name="quantity"/>
<field eval="0.0" name="unit_amount"/>
<field eval="55.75" name="total_amount"/>
<field name="sheet_id" ref="office_furniture_sheet"/>
</record>
@@ -289,10 +216,10 @@
<field name="name">Lamp</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="other_expense_product"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
<field eval="28.99" name="unit_amount"/>
<field eval="1.0" name="quantity"/>
<field eval="0.0" name="unit_amount"/>
<field eval="28.99" name="total_amount"/>
<field name="sheet_id" ref="office_furniture_sheet"/>
</record>
@@ -301,10 +228,10 @@
<field name="name">Car tyres</field>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="other_expense_product"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-03-15'"/>
<field eval="112.58" name="unit_amount"/>
<field eval="4.0" name="quantity"/>
<field eval="0.0" name="unit_amount"/>
<field eval="450.58" name="total_amount"/>
</record>
</data>
+27 -4
View File
@@ -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 <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
values.append(vals)
return values
def get_expenses_to_submit(self):
# if there ere no records selected, then select all draft expenses for the user
if self:
expenses = self.filtered(lambda e: e.state == 'draft' and not e.sheet_id)
else:
expenses = self.env['hr.expense'].search([('state', '=', 'draft'), ('sheet_id', '=', False), ('employee_id', '=', self.env.user.employee_id.id)])
if not expenses:
raise UserError(_('You have no expense to report'))
else:
return expenses.action_submit_expenses()
def action_submit_expenses(self):
context_vals = self._get_default_expense_sheet_values()
if len(context_vals) > 1:
@@ -479,7 +491,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
return {
'name': _('New Expense Reports'),
'type': 'ir.actions.act_window',
'view_mode': 'list,form',
'views': [[False, "list"], [False, "form"]],
'res_model': 'hr.expense.sheet',
'domain': [('id', 'in', sheets.ids)],
'context': self.env.context,
@@ -491,7 +503,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
return {
'name': _('New Expense Report'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'views': [[False, "form"]],
'res_model': 'hr.expense.sheet',
'target': 'current',
'context': context_vals_def,
@@ -841,6 +853,7 @@ class HrExpenseSheet(models.Model):
name = fields.Char('Expense Report Summary', required=True, tracking=True)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', copy=False)
product_ids = fields.Many2many('product.product', compute='_compute_product_ids', search='_search_product_ids', string='Categories')
is_editable = fields.Boolean("Expense Lines Are Editable By Current User", compute='_compute_is_editable')
expense_number = fields.Integer(compute='_compute_expense_number', string='Number of Expenses')
state = fields.Selection([
@@ -956,6 +969,11 @@ class HrExpenseSheet(models.Model):
if expense_lines and any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines):
raise ValidationError(_("Expenses must have the same To Reimburse status."))
@api.depends('expense_line_ids')
def _compute_product_ids(self):
for sheet in self:
sheet.product_ids = sheet.expense_line_ids.mapped('product_id')
@api.constrains('expense_line_ids', 'employee_id')
def _check_employee(self):
for sheet in self:
@@ -969,6 +987,11 @@ class HrExpenseSheet(models.Model):
if any(expense.company_id != sheet.company_id for expense in sheet.expense_line_ids):
raise ValidationError(_('An expense report must contain only lines from the same company.'))
def _search_product_ids(self, operator, value):
if operator == 'in' and not isinstance(value, list):
value = [value]
return [('expense_line_ids.product_id', operator, value)]
@api.model_create_multi
def create(self, vals_list):
context = clean_context(self.env.context)
@@ -3,12 +3,12 @@
import { useService } from '@web/core/utils/hooks';
import session from 'web.session';
const { onWillStart, useState } = owl;
const { Component, onWillStart, useState } = owl;
export class ExpenseDashboard extends Component {
export const ExpenseDashboardMixin = {
setup() {
this._super();
super.setup();
this.orm = useService('orm');
this.state = useState({
@@ -19,7 +19,7 @@ export const ExpenseDashboardMixin = {
const expense_states = await this.orm.call("hr.expense", 'get_expense_dashboard', []);
this.state.expenses = expense_states;
});
},
}
renderMonetaryField(value, currency_id) {
value = value.toFixed(2);
@@ -34,3 +34,4 @@ export const ExpenseDashboardMixin = {
return value;
}
}
ExpenseDashboard.template = 'hr_expense.ExpenseDashboard';
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_expense.ExpenseDashboard" owl="1">
<div class="o_expense_container position-sticky start-0 d-flex o_form_statusbar">
<t t-foreach="Object.entries(state.expenses)" t-as="expense" t-key="expense[0]">
<t t-set="name" t-value="expense[0]"/>
<t t-set="data" t-value="expense[1]"/>
<div t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<span t-esc="renderMonetaryField(data['amount'], data['currency'])" class="h2 m-0 text-odoo"/>
<b class="mx-2" t-esc="data['description']"/>
</div>
<div t-if="name !== 'approved'" t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<i class="fa fa-angle-right fa-3x"/>
</div>
</t>
<div class="fa fa-question-circle-o flex-grow-0.5 d-flex flex-column p-3 border-bottom text-center" t-if="env.debug" data-tooltip="Numbers computed from your personal expenses."/>
</div>
</t>
</templates>
@@ -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);
});
@@ -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);
});
@@ -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;
});
@@ -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() {
@@ -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%;
}
}
+6 -4
View File
@@ -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,
+8 -12
View File
@@ -2,22 +2,18 @@
<templates xml:space="preserve">
<t t-name="hr_expense.KanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<div class="o_expense_container position-sticky start-0 d-flex o_form_statusbar">
<t t-foreach="Object.entries(state.expenses)" t-as="expense" t-key="expense[0]">
<t t-set="name" t-value="expense[0]"/>
<t t-set="data" t-value="expense[1]"/>
<div t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<span t-esc="renderMonetaryField(data['amount'], data['currency'])" class="h2 m-0 text-odoo"/>
<b class="mx-2" t-esc="data['description']"/>
</div>
<div t-if="name !== 'approved'" t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<i class="fa fa-angle-right fa-3x"/>
</div>
</t>
<div t-if="dragState.showDragZone" class="o_dropzone">
<i class="fa fa-upload fa-10x"></i>
</div>
</xpath>
</t>
<t t-name="hr_expense.DashboardKanbanRenderer" t-inherit="hr_expense.KanbanRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<ExpenseDashboard/>
</xpath>
</t>
<t t-name="hr_expense.KanbanButtons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-if='canCreate']" position="after">
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
@@ -0,0 +1,94 @@
/** @odoo-module */
import { ExpenseDashboard } from '../components/expense_dashboard';
import { ExpenseMobileQRCode } from '../mixins/qrcode';
import { ExpenseDocumentUpload, ExpenseDocumentDropZone } from '../mixins/document_upload';
import { registry } from '@web/core/registry';
import { patch } from '@web/core/utils/patch';
import { useService } from '@web/core/utils/hooks';
import { listView } from "@web/views/list/list_view";
import { ListController } from "@web/views/list/list_controller";
import { ListRenderer } from "@web/views/list/list_renderer";
const { onWillStart } = owl;
export class ExpenseListController extends ListController {
setup() {
super.setup();
this.orm = useService('orm');
this.actionService = useService('action');
this.rpc = useService("rpc");
this.user = useService("user");
this.isExpenseSheet = this.model.rootParams.resModel === "hr.expense.sheet";
this.isExpense = this.model.rootParams.resModel === "hr.expense";
onWillStart(async () => {
this.is_expense_team_approver = await this.user.hasGroup("hr_expense.group_hr_expense_team_approver");
this.is_account_invoicing = await this.user.hasGroup("account.group_account_invoice");
});
}
displayCreateReport() {
return this.isExpense;
}
displaySubmit() {
const records = this.model.root.selection;
return records.length && records.every(record => record.data.state === 'draft') && this.isExpenseSheet;
}
displayApprove() {
const records = this.model.root.selection;
return this.is_expense_team_approver && records.length && records.every(record => record.data.state === 'submit') && this.isExpenseSheet;
}
displayPost() {
const records = this.model.root.selection;
return this.is_account_invoicing && records.length && records.every(record => record.data.state === 'approve') && this.isExpenseSheet;
}
async onClick (action) {
const records = this.model.root.selection;
const recordIds = records.map((a) => a.resId);
const model = this.model.rootParams.resModel;
await this.orm.call(model, action, [recordIds]);
// sgv note: we tried this.model.notify(); and does not work
await this.model.root.load();
this.render(true);
}
async onCreateReportClick() {
const records = this.model.root.selection;
const recordIds = records.map((a) => a.resId);
const action = await this.orm.call('hr.expense', 'get_expenses_to_submit', [recordIds]);
this.actionService.doAction(action);
}
}
patch(ExpenseListController.prototype, 'expense_list_controller_upload', ExpenseDocumentUpload);
export class ExpenseListRenderer extends ListRenderer {}
patch(ExpenseListRenderer.prototype, 'expense_list_renderer_qrcode', ExpenseMobileQRCode);
patch(ExpenseListRenderer.prototype, 'expense_list_renderer_qrcode_dzone', ExpenseDocumentDropZone);
ExpenseListRenderer.template = 'hr_expense.ListRenderer';
export class ExpenseDashboardListRenderer extends ExpenseListRenderer {}
ExpenseDashboardListRenderer.components = { ...ExpenseDashboardListRenderer.components, ExpenseDashboard};
ExpenseDashboardListRenderer.template = 'hr_expense.DashboardListRenderer';
registry.category('views').add('hr_expense_tree', {
...listView,
buttonTemplate: 'hr_expense.ListButtons',
Controller: ExpenseListController,
Renderer: ExpenseListRenderer,
});
registry.category('views').add('hr_expense_dashboard_tree', {
...listView,
buttonTemplate: 'hr_expense.ListButtons',
Controller: ExpenseListController,
Renderer: ExpenseDashboardListRenderer,
});
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_expense.ListButtons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<!-- hr.expense and hr.expense.sheet -->
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Scan
</button>
</xpath>
<xpath expr="//div" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput"/>
</xpath>
<!-- hr.expense -->
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayCreateReport()" class="d-none d-md-block btn btn-secondary" t-on-click="onCreateReportClick">
Create Report
</button>
</xpath>
<!-- hr.expense.sheet -->
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displaySubmit()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_submit_sheet')">
Submit
</button>
</xpath>
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayApprove()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('approve_expense_sheets')">
Approve Report
</button>
</xpath>
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayPost()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_sheet_move_create')">
Post Entries
</button>
</xpath>
</t>
<t t-name="hr_expense.ListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
<div t-if="dragState.showDragZone" class="o_dropzone">
<i class="fa fa-upload fa-10x"></i>
</div>
</xpath>
</t>
<t t-name="hr_expense.DashboardListRenderer" t-inherit="hr_expense.ListRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
<ExpenseDashboard/>
</xpath>
</t>
</templates>
@@ -13,6 +13,12 @@
</div>
</t>
<t t-name="hr.expense.DocumentDropZone">
<div class="o_drop_area d-none">
<i class="fa fa-upload fa-10x"></i>
</div>
</t>
<t t-extend="ListView.buttons" t-name="ExpensesListView.buttons">
<t t-jquery="button.o_list_button_add" t-operation="after">
<button type="button" t-att-class="'d-none d-md-block btn' + (!widget.isMobile ? ' btn-secondary' : '') + ' o_button_upload_expense'">
@@ -26,6 +32,13 @@
</button>
</t>
<!-- hr.expense buttons -->
<t t-jquery="button.o_list_button_add" t-operation="after">
<button type="button" t-att-class="'btn btn-secondary' + (widget.isExpense ? '' : ' d-none') + ' o_button_create_report'">
Create Report
</button>
</t>
<t t-jquery="button.o_list_button_add" t-operation="replace">
<button type="button" t-att-class="'btn' + (widget.isMobile ? ' btn-secondary' : ' btn-primary') + ' o_list_button_add'" title="Create record" accesskey="c">
Create
+1 -1
View File
@@ -437,7 +437,7 @@ class TestExpenses(TestExpenseCommon):
self.assertFalse(Expense.env.user.has_group('uom.group_uom'))
# Use a product not using the default uom "Unit(s)"
product = Expense.env.ref('hr_expense.mileage_expense_product')
product = Expense.env.ref('hr_expense.expense_product_mileage')
expense_form = Form(Expense)
expense_form.product_id = product
+83 -195
View File
@@ -44,10 +44,7 @@
<field name="name">hr.expense.tree</field>
<field name="model">hr.expense</field>
<field name="arch" type="xml">
<tree string="Expenses" multi_edit="1" sample="1" js_class="hr_expense_tree_dashboard_upload" decoration-info="state == 'draft'">
<header>
<button name="action_submit_expenses" type="object" string="Create Report"/>
</header>
<tree string="Expenses" multi_edit="1" sample="1" js_class="hr_expense_tree" decoration-info="state == 'draft'">
<field name="company_currency_id" invisible="1"/>
<field name="attachment_number" invisible="True"/>
<field name="date" optional="show"/>
@@ -84,7 +81,7 @@
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<!-- Display the tree dashboard view with the header -->
<attribute name="js_class">hr_expense_tree_dashboard_upload_header</attribute>
<attribute name="js_class">hr_expense_dashboard_tree</attribute>
</xpath>
</field>
</record>
@@ -98,7 +95,7 @@
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<!-- Display the tree dashboard view with the header -->
<attribute name="js_class">hr_expense_tree_dashboard_header</attribute>
<attribute name="js_class">hr_expense_dashboard_tree</attribute>
<attribute name="class">hr_expense h-auto</attribute>
</xpath>
</field>
@@ -159,7 +156,7 @@
<field name="total_amount" widget='monetary' options="{'currency_field': 'currency_id'}" attrs="{'readonly': [('sheet_is_editable', '=', False)]}"/>
<field name="currency_id" groups="base.group_multi_currency"/>
</div>
<label for="total_amount_company" string="" attrs="{'invisible': [('same_currency', '=', True), ('product_has_cost', '=', False)]}"/>
<label for="total_amount_company" attrs="{'invisible': [('product_has_cost', '=', False)]}"/>
<div class="o_row d-flex" attrs="{'invisible': [('same_currency', '=', True), ('product_has_cost', '=', False)]}">
<field name="total_amount_company" style="vertical-align: top;" widget='monetary' options="{'currency_field': 'company_currency_id'}"/>
<field name="label_convert_rate"/>
@@ -284,7 +281,7 @@
</field>
</record>
<!-- Kanban view with header. Used in "All My Expenses -->
<!-- Kanban view with header. Used in "My Expenses -->
<record id="hr_expense_kanban_view_header" model="ir.ui.view">
<field name="name">hr.expense.kanban</field>
<field name="model">hr.expense</field>
@@ -314,9 +311,13 @@
<field name="model">hr.expense</field>
<field name="arch" type="xml">
<graph string="Expenses Analysis" sample="1">
<field name="unit_amount" invisible="1"/>
<field name="quantity" invisible="1"/>
<field name="date"/>
<field name="employee_id"/>
<field name="total_amount" type="measure"/>
<field name="total_amount" type="measure" groups="base.group_multi_currency"/>
<field name="untaxed_amount" groups="base.group_multi_currency"/>
<field name="amount_tax" groups="base.group_multi_currency"/>
</graph>
</field>
</record>
@@ -382,7 +383,7 @@
<record id="hr_expense_actions_all" model="ir.actions.act_window">
<field name="name">Expenses Analysis</field>
<field name="res_model">hr.expense</field>
<field name="view_mode">graph,pivot,tree,kanban,form</field>
<field name="view_mode">graph,pivot,tree,form</field>
<field name="search_view_id" ref="hr_expense_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
@@ -411,57 +412,20 @@
<field name="act_window_id" ref="hr_expense_actions_all"/>
</record>
<record id="hr_expense_actions_all_kanban" model="ir.actions.act_window.view">
<field name="view_mode">kanban</field>
<field name="view_id" ref="hr_expense_view_expenses_analysis_kanban"/>
<field name="act_window_id" ref="hr_expense_actions_all"/>
</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>
<field name="view_mode">tree,kanban,form,graph,pivot,activity</field>
<field name="search_view_id" ref="hr_expense_view_search"/>
<field name="context">{'search_default_my_expenses': 1, 'search_default_no_report': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_expense_receipt">
<h2 class="d-none d-md-block">
Did you try the mobile app?
</h2>
</p>
<p>Snap pictures of your receipts and let Odoo<br/> automatically create expenses for you.</p>
<p class="d-none d-md-block">
<a href="https://apps.apple.com/be/app/odoo/id1272543640" target="_blank" class="o_expense_mobile_app">
<img alt="Apple App Store" class="img img-fluid h-100 o_expense_apple_store" src="/hr_expense/static/img/app_store.png"/>
</a>
<a href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank" class="o_expense_mobile_app">
<img alt="Google Play Store" class="img img-fluid h-100 o_expense_google_store" src="/hr_expense/static/img/play_store.png"/>
</a>
</p>
</field>
</record>
<!-- Tree & Kanban view for "My Expenses to Report" with the header -->
<record id="hr_expense_actions_my_unsubmitted_tree" model="ir.actions.act_window.view">
<field name="view_mode">tree</field>
<field name="view_id" ref="view_my_expenses_tree"/>
<field name="act_window_id" ref="hr_expense_actions_my_unsubmitted"/>
</record>
<record id="hr_expense_actions_my_unsubmitted_kanban" model="ir.actions.act_window.view">
<field name="view_mode">kanban</field>
<field name="view_id" ref="hr_expense_kanban_view_header"/>
<field name="act_window_id" ref="hr_expense_actions_my_unsubmitted"/>
</record>
<record id="hr_expense_actions_my_all" model="ir.actions.act_window">
<field name="name">All My Expenses</field>
<field name="name">My Expenses</field>
<field name="res_model">hr.expense</field>
<field name="view_mode">tree,kanban,form,graph,pivot,activity</field>
<field name="search_view_id" ref="hr_expense_view_search"/>
<field name="context">{'search_default_my_expenses': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_expense_receipt">
<h2 class="d-none d-md-block">
Drag and drop files to create expenses
</h2>
<p>
Or
</p>
<h2 class="d-none d-md-block">
Did you try the mobile app?
</h2>
@@ -520,7 +484,7 @@
<field name="name">product.product.expense.form</field>
<field name="model">product.product</field>
<field name="arch" type="xml">
<form string="Expense Products">
<form string="Expense Categories">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<field name='product_variant_count' invisible='1'/>
@@ -561,7 +525,7 @@
</group>
<notebook>
<page string="Internal Note" name="internal_note">
<field name="description"/>
<field name="description" placeholder="This note will be shown to users when they select this expense product."/>
</page>
</notebook>
</sheet>
@@ -585,30 +549,46 @@
</field>
</record>
<record model="ir.ui.view" id="product_product_expense_categories_tree_view">
<field name="name">product.product.expense.categories.tree.view</field>
<field name="model">product.product</field>
<field name="arch" type="xml">
<tree>
<field name="name" readonly="1"/>
<field name="default_code" optional="show" readonly="1"/>
<field name="description" widget="html" string="Internal Note" optional="show" readonly="1"/>
<field name="lst_price" optional="show" string="Sales Price"/>
<field name="standard_price" optional="show"/>
<field name="supplier_taxes_id" widget="many2many_tags" optional="show"/>
</tree>
</field>
</record>
<record id="hr_expense_product" model="ir.actions.act_window">
<field name="name">Expense Products</field>
<field name="name">Expense Categories</field>
<field name="res_model">product.product</field>
<field name="view_mode">kanban,tree,form</field>
<field name="view_mode">tree,kanban,form</field>
<field name="search_view_id" ref="product.product_search_form_view"/>
<field name="context">{"default_can_be_expensed": 1, 'default_detailed_type': 'service'}</field>
<field name="domain">[('can_be_expensed', '=', True)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No expense products found. Let's create one!
No expense categories found. Let's create one!
</p><p>
Expense products can be reinvoiced to your customers.
Expense categories can be reinvoiced to your customers.
</p>
</field>
</record>
<record id="hr_expense_product_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="product_product_expense_categories_tree_view"/>
<field name="act_window_id" ref="hr_expense_product"/>
</record>
<record id="hr_expense_product_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="sequence" eval="2"/>
<field name="view_mode">kanban</field>
<field name="act_window_id" ref="hr_expense_product"/>
</record>
@@ -624,21 +604,8 @@
<field name="name">hr.expense.sheet.tree</field>
<field name="model">hr.expense.sheet</field>
<field name="arch" type="xml">
<tree string="Expense Reports" multi_edit="1" js_class="hr_expense_tree_dashboard_upload" sample="1" decoration-info="state == 'draft'">
<header>
<button name="approve_expense_sheets" string="Approve Report" type="object"
groups="hr_expense.group_hr_expense_team_approver"
invisible="not context.get('search_default_submitted',0)"/>
<button name="action_sheet_move_create" string="Post Entries" type="object"
groups="account.group_account_invoice,hr_expense.group_hr_expense_manager"
invisible="not context.get('search_default_to_post',0)"/>
<button name="action_register_payment" string="Register Payment" type="object"
groups="account.group_account_invoice,hr_expense.group_hr_expense_manager"
invisible="not context.get('search_default_approved',0)"/>
<button name="action_submit_sheet" string="Submit" type="object"
groups="account.group_account_invoice,hr_expense.group_hr_expense_manager"
invisible="not context.get('search_default_draft',0)"/>
</header>
<tree string="Expense Reports" multi_edit="1" js_class="hr_expense_tree" sample="1" decoration-info="state == 'draft'">
<field name="product_ids" invisible="1"/>
<field name="employee_id" widget="many2one_avatar_employee"/>
<field name="accounting_date" optional="hide" groups="account.group_account_manager"/>
<field name="create_date" optional="hide"/>
@@ -655,19 +622,6 @@
</field>
</record>
<record id="view_hr_expense_sheet_dashboard_tree" model="ir.ui.view">
<field name="name">hr.expense.sheet.dashboard.tree</field>
<field name="model">hr.expense.sheet</field>
<field name="priority">20</field>
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_tree"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="js_class">hr_expense_tree_dashboard</attribute>
</xpath>
</field>
</record>
<!-- Tree view for "My Reports" with header -->
<record id="view_hr_expense_sheet_dashboard_tree_header" model="ir.ui.view">
<field name="name">hr.expense.sheet.dashboard.tree</field>
@@ -677,7 +631,7 @@
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="js_class">hr_expense_tree_dashboard_header</attribute>
<attribute name="js_class">hr_expense_dashboard_tree</attribute>
</xpath>
</field>
</record>
@@ -772,25 +726,33 @@
<notebook>
<page name="expenses" string="Expense">
<field name="is_editable" invisible="1"/>
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft'), ('employee_id', '=', employee_id), ('company_id', '=', company_id)]" options="{'reload_on_button': True}" context="{'form_view_ref' : 'hr_expense.hr_expense_view_form_without_header', 'default_company_id': company_id, 'default_employee_id': employee_id}" attrs="{'readonly': [('is_editable', '=', False)]}">
<field name="expense_line_ids"
nolabel="1"
widget="many2many"
mode="tree,kanban"
domain="[('state', '=', 'draft'), ('employee_id', '=', employee_id), ('company_id', '=', company_id)]"
options="{'reload_on_button': True}"
context="{'form_view_ref' : 'hr_expense.hr_expense_view_form_without_header', 'default_company_id': company_id, 'default_employee_id': employee_id}"
attrs="{'readonly': [('is_editable', '=', False)]}">
<tree decoration-danger="is_refused" editable="bottom" class="o_expense_line_list">
<field name="date" optional="show"/>
<field name="product_id"/>
<field name="name"/>
<field name="description" optional="hide"/>
<field name="state" invisible="1"/>
<field name="attachment_number" invisible="1"/>
<button name="action_get_attachment_view" type="object" icon="fa-paperclip" aria-label="View Attachments" title="View Attachments" class="float-end pe-0" readonly="True" attrs="{'invisible': [('attachment_number', '=', 0)]}"/>
<field name="reference" optional="hide" readonly="True"/>
<field name="analytic_account_id" optional="show" domain="['|', ('company_id', '=', parent.company_id), ('company_id', '=', False)]" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" optional="hide" widget="many2many_tags" groups="analytic.group_analytic_tags"/>
<field name="account_id" optional="hide" readonly="True"/>
<button name="action_get_attachment_view" type="object" icon="fa-paperclip" aria-label="View Attachments" title="View Attachments" class="float-end pe-0" readonly="True"/>
<field name="attachment_number" class="text-end ps-0 pe-4" width="1" nolabel="1"/>
<field name="product_has_cost" invisible="True"/>
<field name="unit_amount" optional="hide" widget="monetary" attrs="{'readonly': [('product_has_cost', '=', False)]}" options="{'currency_field': 'currency_id'}"/>
<field name="currency_id" optional="hide" readonly="True"/>
<field name="quantity" optional="hide" attrs="{'readonly': [('product_has_cost', '=', False)]}"/>
<field name="company_id" invisible="1"/>
<field name="tax_ids" string="Taxes" optional="show" widget="many2many_tags" context="{'default_company_id': company_id}"/>
<field name="amount_tax" optional="hide" context="{'default_company_id': company_id}" readonly="True"/>
<field name="amount_tax" optional="hide" context="{'default_company_id': company_id}" readonly="True" groups="base.group_multi_currency"/>
<field name="total_amount" optional="show" attrs="{'readonly': [('product_has_cost', '=', True)]}"/>
<field name="amount_tax_company" optional="hide" readonly="True"/>
<field name="company_currency_id" invisible="1"/>
@@ -929,12 +891,6 @@
<filter string="My Reports" name="my_reports" domain="[('employee_id.user_id', '=', uid)]"/>
<filter string="My Team" name="my_team_reports" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="hr_expense.group_hr_expense_manager" help="Expenses of Your Team Member"/>
<separator />
<filter domain="[('state', '=', 'draft')]" string="To Submit" name="draft" help="Draft Expenses"/>
<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', '=', 'post')]" string="To Pay" name="approved" help="Expenses to Invoice"/>
<filter domain="[('state', '=', 'cancel')]" string="Refused" name="canceled"/>
<separator/>
<filter string="Date" name="filter_accounting_date" date="accounting_date"/>
<separator/>
<filter domain="[('employee_id.active', '=', False)]" string="Former Employees" name="inactive" groups="hr_expense.group_hr_expense_user,hr_expense.group_hr_expense_manager"/>
@@ -946,7 +902,7 @@
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
]"/>
<group expand="0" string="Group By">
<group expand="0" string="Group By" name="group_filters">
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
<filter string="Department" name="department" domain="[]" context="{'group_by': 'department_id'}"/>
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
@@ -957,6 +913,23 @@
</field>
</record>
<record id="hr_expense_sheet_view_search_with_panel" model="ir.ui.view">
<field name="name">hr.expense.sheet.view.search.with.panel</field>
<field name="model">hr.expense.sheet</field>
<field name="inherit_id" ref="hr_expense_sheet_view_search"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//group[@name='group_filters']" position='after'>
<searchpanel>
<field name="state" expand="1" select="multi" icon="fa-check-square-o" enable_counters="1"/>
<field name="employee_id" limit="20" hierarchize="0" select="one" icon="fa-users"/>
<field name="product_ids" limit="20" select="multi" icon="fa-tags"/>
<field name="company_id" expand="1" icon="fa-building" groups="base.group_multi_company"/>
</searchpanel>
</xpath>
</field>
</record>
<record id="hr_expense_sheet_view_activity" model="ir.ui.view">
<field name="name">hr.expense.sheet.activity</field>
<field name="model">hr.expense.sheet</field>
@@ -1007,79 +980,13 @@
<field name="act_window_id" ref="action_hr_expense_sheet_my_all"/>
</record>
<record id="action_hr_expense_sheet_all_to_submit" model="ir.actions.act_window">
<field name="name">Expense Reports to Submit</field>
<field name="res_model">hr.expense.sheet</field>
<field name="view_mode">tree,kanban,form,pivot,graph,activity</field>
<field name="search_view_id" ref="hr_expense_sheet_view_search"/>
<field name="domain">[]</field>
<field name="context">{'search_default_draft': 1}</field>
<field name="view_id" ref="view_hr_expense_sheet_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No expense reports found
</p><p>
Create the new expense reports from the expenses submitted by the employees you manage.
</p>
</field>
</record>
<record id="action_hr_expense_sheet_all_to_approve" model="ir.actions.act_window">
<field name="name">Expense Reports to Approve</field>
<field name="res_model">hr.expense.sheet</field>
<field name="view_mode">tree,kanban,form,pivot,graph,activity</field>
<field name="search_view_id" ref="hr_expense_sheet_view_search"/>
<field name="domain">[]</field>
<field name="context">{'search_default_submitted': 1}</field>
<field name="view_id" ref="view_hr_expense_sheet_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No expense reports found
</p><p>
Approve the new expense reports submitted by the employees you manage.
</p>
</field>
</record>
<record id="action_hr_expense_sheet_all_to_post" model="ir.actions.act_window">
<field name="name">Expense Reports To Post</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="hr_expense_sheet_view_search"/>
<field name="domain">[]</field>
<field name="context">{'search_default_to_post': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No expense reports found
</p><p>
Post the journal entries of the new expense reports approved by the employees' manager.
</p>
</field>
</record>
<record id="action_hr_expense_sheet_all_to_pay" model="ir.actions.act_window">
<field name="name">Expense Reports To Pay</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="hr_expense_sheet_view_search"/>
<field name="domain">[]</field>
<field name="context">{'search_default_approved': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No expense reports found
</p><p>
Reimburse the employees who incurred these costs or simply register the corresponding payments.
</p>
</field>
</record>
<record id="action_hr_expense_sheet_all" model="ir.actions.act_window">
<field name="name">All 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="hr_expense_sheet_view_search"/>
<field name="search_view_id" ref="hr_expense_sheet_view_search_with_panel"/>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="context">{ 'searchpanel_default_state': ["draft", "submit", "approve", "post", "done"] }</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No expense reports found. Let's create one!
@@ -1157,38 +1064,19 @@
<menuitem id="menu_hr_expense_root" name="Expenses" sequence="230" web_icon="hr_expense,static/description/icon.svg"/>
<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" sequence="1" parent="menu_hr_expense_my_expenses" action="hr_expense_actions_my_unsubmitted" name="My Expenses to Report"/>
<menuitem id="menu_hr_expense_my_expenses_all" sequence="2" parent="menu_hr_expense_my_expenses" action="hr_expense_actions_my_all" name="All My Expenses"/>
<menuitem id="menu_hr_expense_sheet_my_reports" sequence="3" parent="menu_hr_expense_my_expenses" action="action_hr_expense_sheet_my_all" name="My Reports"/>
<menuitem id="menu_hr_expense_my_expenses_all" sequence="1" parent="menu_hr_expense_my_expenses" action="hr_expense_actions_my_all" name="My Expenses"/>
<menuitem id="menu_hr_expense_sheet_my_reports" sequence="2" parent="menu_hr_expense_my_expenses" action="action_hr_expense_sheet_my_all" name="My Reports"/>
<menuitem id="menu_hr_expense_report" name="Expense Reports" sequence="2" parent="menu_hr_expense_root"/>
<menuitem id="menu_hr_expense_sheet_all_to_submit"
name="Reports to Submit" sequence="1" parent="menu_hr_expense_report"
action="action_hr_expense_sheet_all_to_submit"
groups="hr_expense.group_hr_expense_team_approver"/>
<menuitem id="menu_hr_expense_sheet_all_to_approve"
name="Reports to Approve" sequence="2" parent="menu_hr_expense_report"
action="action_hr_expense_sheet_all_to_approve"
groups="hr_expense.group_hr_expense_team_approver"/>
<menuitem id="menu_hr_expense_sheet_all_to_post"
name="Reports to Post" sequence="3" parent="menu_hr_expense_report"
action="action_hr_expense_sheet_all_to_post"
groups="account.group_account_invoice,hr_expense.group_hr_expense_manager"/>
<menuitem id="menu_hr_expense_sheet_all_to_pay"
name="Reports to Pay" sequence="4" parent="menu_hr_expense_report"
action="action_hr_expense_sheet_all_to_pay"
groups="account.group_account_invoice,hr_expense.group_hr_expense_manager"/>
<menuitem id="menu_hr_expense_sheet_all"
name="All Reports" sequence="5" parent="menu_hr_expense_report"
action="action_hr_expense_sheet_all"
groups="account.group_account_invoice,hr_expense.group_hr_expense_manager"/>
<menuitem id="menu_hr_expense_report" name="Expense Reports" sequence="2" parent="menu_hr_expense_root"
action="action_hr_expense_sheet_all"
groups="account.group_account_user,hr_expense.group_hr_expense_team_approver"/>
<menuitem id="menu_hr_expense_reports" name="Reporting" sequence="4" parent="menu_hr_expense_root" groups="hr_expense.group_hr_expense_manager"/>
<menuitem id="menu_hr_expense_all_expenses" name="Expenses Analysis" sequence="0" parent="menu_hr_expense_reports" action="hr_expense_actions_all"/>
<menuitem id="menu_hr_expense_configuration" name="Configuration" parent="menu_hr_expense_root"
sequence="100"/>
<menuitem id="menu_hr_product" name="Expense Products" parent="menu_hr_expense_configuration"
<menuitem id="menu_hr_product" name="Expense Categories" parent="menu_hr_expense_configuration"
action="hr_expense_product" groups="hr_expense.group_hr_expense_manager" sequence="10"/>
<menuitem id="menu_hr_expense_account_employee_expenses" name="Employee Expenses" sequence="22" parent="account.menu_finance_payables" groups="hr_expense.group_hr_expense_user" action="action_hr_expense_account"/>
+1 -1
View File
@@ -15,12 +15,12 @@ This module allow to reinvoice employee expense, by setting the SO directly on t
""",
'depends': ['sale_management', 'hr_expense'],
'data': [
'data/sale_expense_data.xml',
'views/product_view.xml',
'views/hr_expense_views.xml',
'views/sale_order_views.xml',
'views/hr_expense_sheet_views.xml',
],
'demo': ['data/sale_expense_demo.xml'],
'installable': True,
'auto_install': True,
'assets': {
@@ -2,21 +2,21 @@
<odoo>
<data noupdate="1">
<record id="hr_expense.food_expense_product" model="product.product">
<record id="hr_expense.expense_product_meal" model="product.product">
<field name="expense_policy">sales_price</field>
</record>
<record id="hr_expense.mileage_expense_product" model="product.product">
<record id="hr_expense.expense_product_mileage" model="product.product">
<field name="invoice_policy">delivery</field>
<field name="expense_policy">sales_price</field>
</record>
<record id="hr_expense.accomodation_expense_product" model="product.product">
<record id="hr_expense.expense_product_travel_accommodation" model="product.product">
<field name="invoice_policy">delivery</field>
<field name="expense_policy">cost</field>
</record>
<record id="hr_expense.allowance_expense_product" model="product.product">
<record id="hr_expense.expense_product_communication" model="product.product">
<field name="expense_policy">cost</field>
</record>
+22 -1
View File
@@ -1,12 +1,33 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo import api, models, fields, _
class ProductTemplate(models.Model):
_inherit = 'product.template'
expense_policy_tooltip = fields.Char(compute='_compute_expense_policy_tooltip')
@api.depends_context('lang')
@api.depends('expense_policy')
def _compute_expense_policy_tooltip(self):
for product_template in self:
if not product_template.can_be_expensed or not product_template.expense_policy:
product_template.expense_policy_tooltip = False
elif product_template.expense_policy == 'no':
product_template.expense_policy_tooltip = _(
"Expenses of this category may not be added to a Sales Order."
)
elif product_template.expense_policy == 'cost':
product_template.expense_policy_tooltip = _(
"Expenses will be added to the Sales Order at their actual cost when posted."
)
elif product_template.expense_policy == 'sales_price':
product_template.expense_policy_tooltip = _(
"Expenses will be added to the Sales Order at their sales price (product price, pricelist, etc.) when posted."
)
@api.depends('can_be_expensed')
def _compute_visible_expense_policy(self):
expense_products = self.filtered(lambda p: p.can_be_expensed)
@@ -65,7 +65,7 @@
<field name="model">hr.expense.sheet</field>
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='expense_line_ids']/tree/field[@name='name']" position="after">
<xpath expr="//field[@name='expense_line_ids']/tree/field[@name='state']" position="after">
<field name="sale_order_id" attrs="{'invisible': [('can_be_reinvoiced', '=', False)]}" optional="show" options="{'no_create_edit': True, 'no_create': True, 'no_open': True}" context="{'sale_show_partner_name': True, 'sale_expense_all_order': True}"/>
<field name="can_be_reinvoiced" invisible="1"/>
</xpath>
+17 -2
View File
@@ -8,8 +8,11 @@
<field name="arch" type="xml">
<xpath expr="//group[@name='product_details']" position="inside">
<group string="Invoicing">
<field name="invoice_policy" widget="radio"/>
<field name="expense_policy" widget="radio"/>
<label for="expense_policy_tooltip" string=""/>
<div class="o_row">
<field name="expense_policy_tooltip" class="fst-italic text-muted"/>
</div>
</group>
</xpath>
<xpath expr="//field[@name='standard_price']" position="before">
@@ -22,9 +25,21 @@
</field>
</record>
<record id="product_product_view_list_inherit_sale_expense" model="ir.ui.view">
<field name="name">product.product.view.list.inherit.sale.expense</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="hr_expense.product_product_expense_categories_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='supplier_taxes_id']" position="after">
<field name="expense_policy" optional="show"/>
<field name="taxes_id" optional="hide"/>
</xpath>
</field>
</record>
<record id="hr_expense.hr_expense_product" model="ir.actions.act_window">
<field name="context">{"default_can_be_expensed": 1, 'default_detailed_type': 'service',
'default_invoice_policy':'delivery', 'default_expense_policy' : 'cost'}</field>
'default_expense_policy' : 'cost'}</field>
</record>
</odoo>