[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:
@@ -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/**/*',
|
||||
],
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
+5
-4
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', '>', 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"/>
|
||||
|
||||
@@ -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': {
|
||||
|
||||
+4
-4
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user