[REF] *expense*: Technical hr_expense refactor
This refactoring handles a lot of needed changes that should have almost no impact on the UX. The aim is to make the code more readable and coherent Major changes: - Improve tests and make sure they actually make sense and test the flow - Split hr.expense and hr.expense.sheet models into two files - Rename fields according to account naming conventions - all .*_amount become *_amount_currency and .*_amount_company -> .*_amount - unit_amount -> price_unit - all .*_amount or .*number.* for non-monetary -> nb_* - amount_tax -> tax_amount - Move hr.expense.sample field to hr_expense_extract - Replace hr.expense.same_currency field by hr.expense.is_multiple_currency to be consistent with hr.expense.sheet - Remove hr.expense.unit_amount_display deprecated field - Remove OCR-related & deprecated reference field - Remove hr.expense.sheet.address_id deprecated field - Simplify security fields (removing sheet_is_editable and merging the behaviour for fewer confusions) - Edit product fields to use related fields when possible Minor changes: - Coding UTF-8 lines removal - Cleaning some imports - Making some formatting more consistent task-3358176 closes odoo/odoo#130244 Related: odoo/upgrade#5016 Related: odoo/enterprise#45149 Signed-off-by: Quentin De Paoli <qdp@odoo.com>
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
|
||||
<field name="product_id" ref="product_product_no_cost"/>
|
||||
<field eval="289.0" name="total_amount"/>
|
||||
<field eval="289.0" name="total_amount_currency"/>
|
||||
<field name="date" eval="time.strftime('%Y')+'-04-03'"/>
|
||||
</record>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
|
||||
<field name="product_id" ref="product_product_no_cost"/>
|
||||
<field eval="889.0" name="total_amount"/>
|
||||
<field eval="889.0" name="total_amount_currency"/>
|
||||
<field name="date" eval="time.strftime('%Y')+'-04-03'"/>
|
||||
</record>
|
||||
|
||||
@@ -53,17 +53,15 @@
|
||||
<field name="name">Travel by car</field>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="product_id" ref="expense_product_mileage"/>
|
||||
<field eval="108.84" name="total_amount"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_km"/>
|
||||
<field eval="320.0" name="quantity"/>
|
||||
<field eval="108.84" name="quantity"/>
|
||||
</record>
|
||||
|
||||
<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="expense_product_meal"/>
|
||||
<field eval="20" name="total_amount"/>
|
||||
<field eval="1.0" name="quantity"/>
|
||||
<field eval="20" name="total_amount_currency"/>
|
||||
</record>
|
||||
|
||||
<record id="travel_ny_sheet" model="hr.expense.sheet">
|
||||
@@ -77,9 +75,8 @@
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
|
||||
<field name="product_id" ref="expense_product_travel_accommodation"/>
|
||||
<field eval="700.0" name="total_amount"/>
|
||||
<field eval="700.0" name="total_amount_currency"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field eval="1.0" name="quantity"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m')+'-12'"/>
|
||||
<field name="sheet_id" ref="travel_ny_sheet"/>
|
||||
</record>
|
||||
@@ -89,9 +86,8 @@
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="expense_product_travel_accommodation"/>
|
||||
<field eval="2000.0" name="total_amount"/>
|
||||
<field eval="2000.0" name="total_amount_currency"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field eval="5.0" name="quantity"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m')+'-17'"/>
|
||||
<field name="sheet_id" ref="travel_ny_sheet"/>
|
||||
</record>
|
||||
@@ -101,9 +97,8 @@
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="expense_product_meal"/>
|
||||
<field eval="152.8" name="total_amount"/>
|
||||
<field eval="152.8" name="total_amount_currency"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m')+'-13'"/>
|
||||
<field eval="1.0" name="quantity"/>
|
||||
<field name="sheet_id" ref="travel_ny_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -113,8 +108,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="expense_product_meal"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m')+'-15'"/>
|
||||
<field eval="56.8" name="total_amount"/>
|
||||
<field eval="1.0" name="quantity"/>
|
||||
<field eval="56.8" name="total_amount_currency"/>
|
||||
<field name="sheet_id" ref="travel_ny_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -130,10 +124,9 @@
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
|
||||
<field name="product_id" ref="expense_product_mileage"/>
|
||||
<field eval="120.85" name="total_amount"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_km"/>
|
||||
<field name="date" eval="time.strftime('%Y')+'-01-15'"/>
|
||||
<field eval="152.0" name="quantity"/>
|
||||
<field eval="120.85" name="quantity"/>
|
||||
<field name="sheet_id" ref="customer_meeting_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -143,7 +136,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="product_product_no_cost"/>
|
||||
<field name="date" eval="time.strftime('%Y')+'-01-15'"/>
|
||||
<field eval="152.8" name="total_amount"/>
|
||||
<field eval="152.8" name="total_amount_currency"/>
|
||||
<field name="sheet_id" ref="customer_meeting_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -160,8 +153,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="expense_product_meal"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
|
||||
<field eval="154" name="total_amount"/>
|
||||
<field eval="12.0" name="quantity"/>
|
||||
<field eval="154" name="total_amount_currency"/>
|
||||
<field name="sheet_id" ref="team_building_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -171,8 +163,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="expense_product_meal"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
|
||||
<field eval="42.5" name="total_amount"/>
|
||||
<field eval="17.0" name="quantity"/>
|
||||
<field eval="42.5" name="total_amount_currency"/>
|
||||
<field name="sheet_id" ref="team_building_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -182,7 +173,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="product_product_no_cost"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
|
||||
<field eval="25" name="total_amount"/>
|
||||
<field eval="25" name="total_amount_currency"/>
|
||||
<field name="sheet_id" ref="team_building_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -199,7 +190,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="product_product_no_cost"/>
|
||||
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
|
||||
<field eval="55.75" name="total_amount"/>
|
||||
<field eval="55.75" name="total_amount_currency"/>
|
||||
<field name="sheet_id" ref="office_furniture_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -209,7 +200,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="product_product_no_cost"/>
|
||||
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
|
||||
<field eval="28.99" name="total_amount"/>
|
||||
<field eval="28.99" name="total_amount_currency"/>
|
||||
<field name="sheet_id" ref="office_furniture_sheet"/>
|
||||
</record>
|
||||
|
||||
@@ -220,7 +211,7 @@
|
||||
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
|
||||
<field name="product_id" ref="product_product_no_cost"/>
|
||||
<field name="date" eval="time.strftime('%Y')+'-03-15'"/>
|
||||
<field eval="450.58" name="total_amount"/>
|
||||
<field eval="450.58" name="total_amount_currency"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<p>The first word of the email subject did not correspond to any category code. You'll have to set the category manually on the expense.</p>
|
||||
</div>
|
||||
<p>
|
||||
Price: <t t-out="expense.unit_amount"/><t t-out="expense.currency_id.symbol"/>
|
||||
Price: <t t-out="expense.price_unit"/><t t-out="expense.currency_id.symbol"/>
|
||||
</p>
|
||||
<p t-if="expense.employee_id.user_id">
|
||||
<br/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_employee
|
||||
@@ -8,6 +7,7 @@ from . import account_payment
|
||||
from . import account_tax
|
||||
from . import hr_department
|
||||
from . import hr_expense
|
||||
from . import hr_expense_sheet
|
||||
from . import product_template
|
||||
from . import res_config_settings
|
||||
from . import account_journal_dashboard
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools.misc import formatLang
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
|
||||
@@ -10,7 +10,7 @@ from odoo.tools.misc import frozendict
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
expense_sheet_id = fields.Many2one('hr.expense.sheet', ondelete='set null', copy=False)
|
||||
expense_sheet_id = fields.Many2one(comodel_name='hr.expense.sheet', ondelete='set null', copy=False)
|
||||
|
||||
def action_open_expense_report(self):
|
||||
self.ensure_one()
|
||||
@@ -44,8 +44,7 @@ class AccountMove(models.Model):
|
||||
frozendict(
|
||||
{
|
||||
"move_id": move.id,
|
||||
"date_maturity": move.expense_sheet_id.accounting_date
|
||||
or fields.Date.context_today(move.expense_sheet_id),
|
||||
"date_maturity": move.expense_sheet_id.accounting_date or fields.Date.context_today(move.expense_sheet_id),
|
||||
}
|
||||
): {
|
||||
"balance": -sum(term_lines.mapped("balance")),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
@@ -10,7 +9,7 @@ class Employee(models.Model):
|
||||
def _group_hr_expense_user_domain(self):
|
||||
# We return the domain only if the group exists for the following reason:
|
||||
# When a group is created (at module installation), the `res.users` form view is
|
||||
# automatically modifiedto add application accesses. When modifiying the view, it
|
||||
# automatically modified to add application accesses. When modifying the view, it
|
||||
# reads the related field `expense_manager_id` of `res.users` and retrieve its domain.
|
||||
# This is a problem because the `group_hr_expense_user` record has already been created but
|
||||
# not its associated `ir.model.data` which makes `self.env.ref(...)` fail.
|
||||
@@ -18,11 +17,13 @@ class Employee(models.Model):
|
||||
return [('groups_id', 'in', group.ids)] if group else []
|
||||
|
||||
expense_manager_id = fields.Many2one(
|
||||
'res.users', string='Expense',
|
||||
domain=_group_hr_expense_user_domain,
|
||||
comodel_name='res.users',
|
||||
string='Expense',
|
||||
compute='_compute_expense_manager', store=True, readonly=False,
|
||||
domain=_group_hr_expense_user_domain,
|
||||
help='Select the user responsible for approving "Expenses" of this employee.\n'
|
||||
'If empty, the approval is done by an Administrator or Approver (determined in settings/users).')
|
||||
'If empty, the approval is done by an Administrator or Approver (determined in settings/users).',
|
||||
)
|
||||
|
||||
filter_for_expense = fields.Boolean(store=False, search='_search_filter_for_expense')
|
||||
|
||||
@@ -31,7 +32,8 @@ class Employee(models.Model):
|
||||
for employee in self:
|
||||
previous_manager = employee._origin.parent_id.user_id
|
||||
manager = employee.parent_id.user_id
|
||||
if manager and manager.has_group('hr_expense.group_hr_expense_user') and (employee.expense_manager_id == previous_manager or not employee.expense_manager_id):
|
||||
if manager and manager.has_group('hr_expense.group_hr_expense_user') \
|
||||
and (employee.expense_manager_id == previous_manager or not employee.expense_manager_id):
|
||||
employee.expense_manager_id = manager
|
||||
elif not employee.expense_manager_id:
|
||||
employee.expense_manager_id = False
|
||||
@@ -43,7 +45,7 @@ class Employee(models.Model):
|
||||
assert operator == '='
|
||||
assert value
|
||||
|
||||
res = [('id', '=', 0)] # Nothing accepted by domain, by default
|
||||
res = [('id', '=', 0)] # Nothing accepted by domain, by default
|
||||
if self.user_has_groups('hr_expense.group_hr_expense_user') or self.user_has_groups('account.group_account_user'):
|
||||
res = ['|', ('company_id', '=', False), ('company_id', 'child_of', self.env.company.root_id.id)] # Then, domain accepts everything
|
||||
elif self.user_has_groups('hr_expense.group_hr_expense_team_approver') and self.env.user.employee_ids:
|
||||
@@ -62,6 +64,7 @@ class Employee(models.Model):
|
||||
res = [('id', '=', employee.id), '|', ('company_id', '=', False), ('company_id', '=', employee.company_id.id)]
|
||||
return res
|
||||
|
||||
|
||||
class EmployeePublic(models.Model):
|
||||
_inherit = 'hr.employee.public'
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,745 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, Command, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
|
||||
class HrExpenseSheet(models.Model):
|
||||
"""
|
||||
Here are the rights associated with the expense flow
|
||||
|
||||
Action Group Restriction
|
||||
=================================================================================
|
||||
Submit Employee Only his own
|
||||
Officer If he is expense manager of the employee, manager of the employee
|
||||
or the employee is in the department managed by the officer
|
||||
Manager Always
|
||||
Approve Officer Not his own and he is expense manager of the employee, manager of the employee
|
||||
or the employee is in the department managed by the officer
|
||||
Manager Always
|
||||
Post Anybody State = approve and journal_id defined
|
||||
Done Anybody State = approve and journal_id defined
|
||||
Cancel Officer Not his own and he is expense manager of the employee, manager of the employee
|
||||
or the employee is in the department managed by the officer
|
||||
Manager Always
|
||||
=================================================================================
|
||||
"""
|
||||
_name = "hr.expense.sheet"
|
||||
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin']
|
||||
_description = "Expense Report"
|
||||
_order = "accounting_date desc, id desc"
|
||||
_check_company_auto = True
|
||||
|
||||
@api.model
|
||||
def _default_employee_id(self):
|
||||
return self.env.user.employee_id
|
||||
|
||||
@api.model
|
||||
def _default_journal_id(self):
|
||||
"""
|
||||
The journal is determining the company of the accounting entries generated from expense.
|
||||
We need to force journal company and expense sheet company to be the same.
|
||||
"""
|
||||
company_journal_id = self.env.company.expense_journal_id
|
||||
if company_journal_id:
|
||||
return company_journal_id.id
|
||||
default_company_id = self.default_get(['company_id'])['company_id']
|
||||
journal = self.env['account.journal'].search([
|
||||
*self.env['account.journal']._check_company_domain(default_company_id),
|
||||
('type', '=', 'purchase'),
|
||||
], limit=1)
|
||||
return journal.id
|
||||
|
||||
name = fields.Char(string="Expense Report Summary", required=True, tracking=True)
|
||||
expense_line_ids = fields.One2many(
|
||||
comodel_name='hr.expense', inverse_name='sheet_id',
|
||||
string="Expense Lines",
|
||||
copy=False,
|
||||
)
|
||||
nb_expense = fields.Integer(compute='_compute_nb_expense', string="Number of Expenses")
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
('draft', 'To Submit'),
|
||||
('submit', 'Submitted'),
|
||||
('approve', 'Approved'),
|
||||
('post', 'Posted'),
|
||||
('done', 'Done'),
|
||||
('cancel', 'Refused')
|
||||
],
|
||||
string="Status",
|
||||
compute='_compute_state', store=True, readonly=True,
|
||||
index=True,
|
||||
required=True,
|
||||
default='draft',
|
||||
tracking=True,
|
||||
copy=False,
|
||||
)
|
||||
approval_state = fields.Selection(
|
||||
selection=[
|
||||
('submit', 'Submitted'),
|
||||
('approve', 'Approved'),
|
||||
('cancel', 'Refused'),
|
||||
],
|
||||
copy=False,
|
||||
)
|
||||
approval_date = fields.Datetime(string="Approval Date", readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company',
|
||||
string="Company",
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
employee_id = fields.Many2one(
|
||||
comodel_name='hr.employee',
|
||||
string="Employee",
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=_default_employee_id,
|
||||
domain=[('filter_for_expense', '=', True)],
|
||||
check_company=True,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
department_id = fields.Many2one(
|
||||
comodel_name='hr.department',
|
||||
related='employee_id.department_id',
|
||||
string="Department",
|
||||
store=True,
|
||||
copy=False,
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
comodel_name='res.users',
|
||||
string="Manager",
|
||||
compute='_compute_from_employee_id', store=True, readonly=True,
|
||||
domain=lambda self: [('groups_id', 'in', self.env.ref('hr_expense.group_hr_expense_team_approver').id)],
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
product_ids = fields.Many2many(
|
||||
comodel_name='product.product',
|
||||
string="Categories",
|
||||
compute='_compute_product_ids',
|
||||
search='_search_product_ids',
|
||||
check_company=True,
|
||||
)
|
||||
|
||||
# === Amount fields === #
|
||||
total_amount = fields.Monetary(
|
||||
string="Total",
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_amount', store=True, readonly=True,
|
||||
tracking=True,
|
||||
)
|
||||
untaxed_amount = fields.Monetary(
|
||||
string="Untaxed Amount",
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_amount', store=True, readonly=True,
|
||||
)
|
||||
total_tax_amount = fields.Monetary(
|
||||
string="Taxes",
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_amount', store=True, readonly=True,
|
||||
)
|
||||
amount_residual = fields.Monetary(
|
||||
string="Amount Due",
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_from_account_move_ids', store=True, readonly=True,
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
string="Currency",
|
||||
compute='_compute_currency_id', store=True, readonly=True,
|
||||
)
|
||||
company_currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
related='company_id.currency_id',
|
||||
string="Report Company Currency"
|
||||
)
|
||||
is_multiple_currency = fields.Boolean(
|
||||
string="Handle lines with different currencies",
|
||||
compute='_compute_is_multiple_currency',
|
||||
)
|
||||
|
||||
# === Account fields === #
|
||||
payment_state = fields.Selection(
|
||||
selection=lambda self: self.env["account.move"]._fields["payment_state"].selection,
|
||||
string="Payment Status",
|
||||
compute='_compute_from_account_move_ids', store=True, readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
payment_mode = fields.Selection(
|
||||
related='expense_line_ids.payment_mode',
|
||||
string="Paid By",
|
||||
tracking=True,
|
||||
readonly=True,
|
||||
)
|
||||
employee_journal_id = fields.Many2one(
|
||||
comodel_name='account.journal',
|
||||
string="Journal",
|
||||
default=_default_journal_id,
|
||||
check_company=True,
|
||||
domain=[('type', '=', 'purchase')],
|
||||
help="The journal used when the expense is paid by employee.",
|
||||
)
|
||||
selectable_payment_method_line_ids = fields.Many2many(
|
||||
comodel_name='account.payment.method.line',
|
||||
compute='_compute_selectable_payment_method_line_ids',
|
||||
)
|
||||
payment_method_line_id = fields.Many2one(
|
||||
comodel_name='account.payment.method.line',
|
||||
string="Payment Method",
|
||||
compute='_compute_payment_method_line_id', store=True, readonly=False,
|
||||
domain="[('id', 'in', selectable_payment_method_line_ids)]",
|
||||
help="The payment method used when the expense is paid by the company.",
|
||||
)
|
||||
accounting_date = fields.Date(string="Accounting Date", compute='_compute_accounting_date', store=True)
|
||||
account_move_ids = fields.One2many(
|
||||
string="Journal Entries",
|
||||
comodel_name='account.move', inverse_name='expense_sheet_id', readonly=True,
|
||||
)
|
||||
nb_account_move = fields.Integer(string="Number of Journal Entries", compute='_compute_nb_account_move')
|
||||
journal_id = fields.Many2one(
|
||||
comodel_name='account.journal',
|
||||
string="Expense Journal",
|
||||
compute='_compute_journal_id', store=True,
|
||||
check_company=True,
|
||||
)
|
||||
|
||||
# === Security fields === #
|
||||
can_reset = fields.Boolean(string='Can Reset', compute='_compute_can_reset')
|
||||
can_approve = fields.Boolean(string='Can Approve', compute='_compute_can_approve')
|
||||
cannot_approve_reason = fields.Char(string='Cannot Approve Reason', compute='_compute_can_approve')
|
||||
is_editable = fields.Boolean(string="Expense Lines Are Editable By Current User", compute='_compute_is_editable')
|
||||
|
||||
_sql_constraints = [(
|
||||
'journal_id_required_posted',
|
||||
"CHECK((state IN ('post', 'done') AND journal_id IS NOT NULL) OR (state NOT IN ('post', 'done')))",
|
||||
'The journal must be set on posted expense'
|
||||
)]
|
||||
|
||||
@api.depends('expense_line_ids.total_amount', 'expense_line_ids.tax_amount')
|
||||
def _compute_amount(self):
|
||||
for sheet in self:
|
||||
sheet.total_amount = sum(sheet.expense_line_ids.mapped('total_amount'))
|
||||
sheet.total_tax_amount = sum(sheet.expense_line_ids.mapped('tax_amount'))
|
||||
sheet.untaxed_amount = sheet.total_amount - sheet.total_tax_amount
|
||||
|
||||
@api.depends('account_move_ids.payment_state', 'account_move_ids.amount_residual')
|
||||
def _compute_from_account_move_ids(self):
|
||||
for sheet in self:
|
||||
if sheet.payment_mode == 'company_account':
|
||||
if sheet.account_move_ids:
|
||||
# when the sheet is paid by the company, the state/amount of the related account_move_ids are not relevant
|
||||
# unless all moves have been reversed
|
||||
sheet.amount_residual = 0.
|
||||
if sheet.account_move_ids - sheet.account_move_ids.filtered('reversal_move_id'):
|
||||
sheet.payment_state = 'paid'
|
||||
else:
|
||||
sheet.payment_state = 'reversed'
|
||||
else:
|
||||
sheet.amount_residual = sum(sheet.account_move_ids.mapped('amount_residual'))
|
||||
payment_states = set(sheet.account_move_ids.mapped('payment_state'))
|
||||
if len(payment_states) <= 1: # If only 1 move or only one state
|
||||
sheet.payment_state = payment_states.pop() if payment_states else 'not_paid'
|
||||
elif 'partial' in payment_states or 'paid' in payment_states: # else if any are (partially) paid
|
||||
sheet.payment_state = 'partial'
|
||||
else:
|
||||
sheet.payment_state = 'not_paid'
|
||||
else:
|
||||
# Only one move is created when the expenses are paid by the employee
|
||||
if sheet.account_move_ids:
|
||||
sheet.amount_residual = sum(sheet.account_move_ids.mapped('amount_residual'))
|
||||
sheet.payment_state = sheet.account_move_ids[:1].payment_state
|
||||
else:
|
||||
sheet.amount_residual = 0.0
|
||||
sheet.payment_state = 'not_paid'
|
||||
|
||||
@api.depends('selectable_payment_method_line_ids')
|
||||
def _compute_payment_method_line_id(self):
|
||||
for sheet in self:
|
||||
sheet.payment_method_line_id = sheet.selectable_payment_method_line_ids._origin[:1]
|
||||
|
||||
@api.depends('employee_journal_id', 'payment_method_line_id')
|
||||
def _compute_journal_id(self):
|
||||
for sheet in self:
|
||||
if sheet.payment_mode == 'company_account':
|
||||
sheet.journal_id = sheet.payment_method_line_id.journal_id
|
||||
else:
|
||||
sheet.journal_id = sheet.employee_journal_id
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_selectable_payment_method_line_ids(self):
|
||||
for sheet in self:
|
||||
allowed_method_line_ids = sheet.company_id.company_expense_allowed_payment_method_line_ids
|
||||
if allowed_method_line_ids:
|
||||
sheet.selectable_payment_method_line_ids = allowed_method_line_ids
|
||||
else:
|
||||
sheet.selectable_payment_method_line_ids = self.env['account.payment.method.line'].search([
|
||||
('payment_type', '=', 'outbound'),
|
||||
('company_id', '=', sheet.company_id.id)
|
||||
])
|
||||
|
||||
@api.depends('account_move_ids', 'payment_state', 'approval_state')
|
||||
def _compute_state(self):
|
||||
for sheet in self:
|
||||
if sheet.payment_state != 'not_paid':
|
||||
sheet.state = 'done'
|
||||
elif sheet.account_move_ids:
|
||||
sheet.state = 'post'
|
||||
elif sheet.approval_state:
|
||||
sheet.state = sheet.approval_state
|
||||
else:
|
||||
sheet.state = 'draft'
|
||||
|
||||
@api.depends('expense_line_ids.currency_id', 'company_currency_id')
|
||||
def _compute_currency_id(self):
|
||||
for sheet in self:
|
||||
if not sheet.expense_line_ids or sheet.is_multiple_currency or sheet.payment_mode == 'own_account':
|
||||
sheet.currency_id = sheet.company_currency_id
|
||||
else:
|
||||
sheet.currency_id = sheet.expense_line_ids[:1].currency_id
|
||||
|
||||
@api.depends('expense_line_ids.currency_id')
|
||||
def _compute_is_multiple_currency(self):
|
||||
for sheet in self:
|
||||
sheet.is_multiple_currency = any(sheet.expense_line_ids.mapped('is_multiple_currency')) \
|
||||
or len(sheet.expense_line_ids.mapped('currency_id')) > 1
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_can_reset(self):
|
||||
is_expense_user = self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
for sheet in self:
|
||||
sheet.can_reset = is_expense_user if is_expense_user else sheet.employee_id.user_id == self.env.user
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('employee_id')
|
||||
def _compute_can_approve(self):
|
||||
is_team_approver = self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
is_approver = self.user_has_groups('hr_expense.group_hr_expense_user')
|
||||
is_hr_admin = self.user_has_groups('hr_expense.group_hr_expense_manager')
|
||||
|
||||
for sheet in self:
|
||||
reason = False
|
||||
if not is_team_approver:
|
||||
reason = _("%s: Your are not a Manager or HR Officer", sheet.name)
|
||||
|
||||
elif not is_hr_admin:
|
||||
sheet_employee = sheet.employee_id
|
||||
current_managers = sheet_employee.expense_manager_id \
|
||||
| sheet_employee.parent_id.user_id \
|
||||
| sheet_employee.department_id.manager_id.user_id \
|
||||
| sheet.user_id
|
||||
|
||||
if sheet_employee.user_id == self.env.user:
|
||||
reason = _("%s: It is your own expense", sheet.name)
|
||||
|
||||
elif self.env.user not in current_managers and not is_approver and sheet_employee.expense_manager_id.id != self.env.user.id:
|
||||
reason = _("%s: It is not from your department", sheet.name)
|
||||
|
||||
sheet.can_approve = not reason
|
||||
sheet.cannot_approve_reason = reason
|
||||
|
||||
@api.depends('expense_line_ids')
|
||||
def _compute_nb_expense(self):
|
||||
for sheet in self:
|
||||
sheet.nb_expense = len(sheet.expense_line_ids)
|
||||
|
||||
@api.depends('account_move_ids')
|
||||
def _compute_nb_account_move(self):
|
||||
for sheet in self:
|
||||
sheet.nb_account_move = len(sheet.account_move_ids)
|
||||
|
||||
@api.depends('account_move_ids.date')
|
||||
def _compute_accounting_date(self):
|
||||
for sheet in self.filtered('account_move_ids'):
|
||||
sheet.accounting_date = sheet.account_move_ids[:1].date
|
||||
|
||||
@api.depends('employee_id', 'employee_id.department_id')
|
||||
def _compute_from_employee_id(self):
|
||||
for sheet in self:
|
||||
sheet.department_id = sheet.employee_id.department_id
|
||||
sheet.user_id = sheet.employee_id.expense_manager_id or sheet.employee_id.parent_id.user_id
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('employee_id', 'user_id', 'state')
|
||||
def _compute_is_editable(self):
|
||||
is_hr_admin = self.user_has_groups('hr_expense.group_hr_expense_manager')
|
||||
is_approver = self.user_has_groups('hr_expense.group_hr_expense_user')
|
||||
for sheet in self:
|
||||
if sheet.state not in {'draft', 'submit', 'approve'}:
|
||||
# Not editable
|
||||
sheet.is_editable = False
|
||||
continue
|
||||
|
||||
employee = sheet.employee_id
|
||||
|
||||
is_own_sheet = employee.user_id == self.env.user
|
||||
if is_own_sheet and sheet.state == 'draft':
|
||||
# Anyone can edit their own draft sheet
|
||||
sheet.is_editable = True
|
||||
continue
|
||||
|
||||
managers = employee.expense_manager_id | employee.parent_id.user_id | employee.department_id.manager_id.user_id
|
||||
if is_approver:
|
||||
managers |= self.env.user
|
||||
if not is_own_sheet and self.env.user in managers:
|
||||
# If Approver-level or designated manager, can edit other people sheet
|
||||
sheet.is_editable = True
|
||||
continue
|
||||
|
||||
if is_hr_admin and sheet.state in {'draft', 'submit', 'approve'}:
|
||||
# Administrator-level users are not restricted
|
||||
sheet.is_editable = True
|
||||
continue
|
||||
sheet.is_editable = False
|
||||
|
||||
@api.constrains('expense_line_ids')
|
||||
def _check_payment_mode(self):
|
||||
for sheet in self:
|
||||
expense_lines = sheet.mapped('expense_line_ids')
|
||||
if expense_lines and any(expense.payment_mode != expense_lines[:1].payment_mode for expense in expense_lines):
|
||||
raise ValidationError(_("All expenses in an expense report must have the same \"paid by\" criteria."))
|
||||
|
||||
@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:
|
||||
if sheet.expense_line_ids.employee_id - sheet.employee_id:
|
||||
raise ValidationError(_('You cannot add expenses of another employee.'))
|
||||
|
||||
@api.constrains('expense_line_ids', 'company_id')
|
||||
def _check_expense_lines_company(self):
|
||||
for sheet in self:
|
||||
if sheet.expense_line_ids.company_id - sheet.company_id:
|
||||
raise ValidationError(_('An expense report must contain only lines from the same company.'))
|
||||
|
||||
@api.model
|
||||
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)]
|
||||
|
||||
# ----------------------------------------
|
||||
# ORM Overrides
|
||||
# ----------------------------------------
|
||||
|
||||
def _read_format(self, fnames, load='_classic_read'):
|
||||
# setting the context in the field on the view is not enough
|
||||
self = self.with_context(show_payment_journal_id=True)
|
||||
return super()._read_format(fnames, load)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
context = clean_context(self.env.context)
|
||||
context.update({
|
||||
'mail_create_nosubscribe': True,
|
||||
'mail_auto_subscribe_no_notify': True,
|
||||
})
|
||||
sheets = super(HrExpenseSheet, self.with_context(context)).create(vals_list)
|
||||
sheets.activity_update()
|
||||
return sheets
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_posted_or_paid(self):
|
||||
for expense in self:
|
||||
if expense.state in {'post', 'done'}:
|
||||
raise UserError(_('You cannot delete a posted or paid expense.'))
|
||||
|
||||
# --------------------------------------------
|
||||
# Mail Thread
|
||||
# --------------------------------------------
|
||||
|
||||
def _get_mail_thread_data_attachments(self):
|
||||
"""
|
||||
In order to see in the sheet attachment preview the corresponding
|
||||
expenses' attachments, the latter attachments are added to the fetched data for the sheet record.
|
||||
"""
|
||||
self.ensure_one()
|
||||
res = super()._get_mail_thread_data_attachments()
|
||||
expense_attachments = self.env['ir.attachment'].search(
|
||||
[('res_id', 'in', self.expense_line_ids.ids), ('res_model', '=', 'hr.expense')],
|
||||
order='id desc',
|
||||
)
|
||||
return res | expense_attachments
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'state' in init_values and self.state == 'draft':
|
||||
return self.env.ref('hr_expense.mt_expense_reset')
|
||||
if 'state' in init_values and self.state == 'approve':
|
||||
if init_values['state'] in {'post', 'done'}:
|
||||
return self.env.ref('hr_expense.mt_expense_entry_delete')
|
||||
return self.env.ref('hr_expense.mt_expense_approved')
|
||||
if 'state' in init_values and self.state == 'cancel':
|
||||
return self.env.ref('hr_expense.mt_expense_refused')
|
||||
if 'state' in init_values and self.state == 'done':
|
||||
return self.env.ref('hr_expense.mt_expense_paid')
|
||||
return super()._track_subtype(init_values)
|
||||
|
||||
def _message_auto_subscribe_followers(self, updated_values, subtype_ids):
|
||||
res = super()._message_auto_subscribe_followers(updated_values, subtype_ids)
|
||||
if updated_values.get('employee_id'):
|
||||
employee_user = self.env['hr.employee'].browse(updated_values['employee_id']).user_id
|
||||
if employee_user:
|
||||
res.append((employee_user.partner_id.id, subtype_ids, False))
|
||||
return res
|
||||
|
||||
def activity_update(self):
|
||||
reports_requiring_feedback = self.env['hr.expense.sheet']
|
||||
reports_activity_unlink = self.env['hr.expense.sheet']
|
||||
for expense_report in self:
|
||||
if expense_report.state == 'submit':
|
||||
expense_report.activity_schedule(
|
||||
'hr_expense.mail_act_expense_approval',
|
||||
user_id=expense_report.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif expense_report.state == 'approve':
|
||||
reports_requiring_feedback |= expense_report
|
||||
elif expense_report.state in {'draft', 'cancel'}:
|
||||
reports_activity_unlink |= expense_report
|
||||
if reports_requiring_feedback:
|
||||
reports_requiring_feedback.activity_feedback(['hr_expense.mail_act_expense_approval'])
|
||||
if reports_activity_unlink:
|
||||
reports_activity_unlink.activity_unlink(['hr_expense.mail_act_expense_approval'])
|
||||
|
||||
# --------------------------------------------
|
||||
# Actions
|
||||
# --------------------------------------------
|
||||
|
||||
def action_submit_sheet(self):
|
||||
self._do_submit()
|
||||
|
||||
def action_approve_expense_sheets(self):
|
||||
self._check_can_approve()
|
||||
self._validate_analytic_distribution()
|
||||
duplicates = self.expense_line_ids.duplicate_expense_ids.filtered(lambda exp: exp.state in {'approved', 'done'})
|
||||
if duplicates:
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id('hr_expense.hr_expense_approve_duplicate_action')
|
||||
action['context'] = {'default_sheet_ids': self.ids, 'default_expense_ids': duplicates.ids}
|
||||
return action
|
||||
self._do_approve()
|
||||
|
||||
def action_refuse_expense_sheets(self):
|
||||
self._check_can_refuse()
|
||||
return self.env["ir.actions.act_window"]._for_xml_id('hr_expense.hr_expense_refuse_wizard_action')
|
||||
|
||||
def action_reset_approval_expense_sheets(self):
|
||||
self._check_can_reset_approval()
|
||||
self._do_reset_approval()
|
||||
|
||||
def action_sheet_move_create(self):
|
||||
self._check_can_create_move()
|
||||
self._do_create_moves()
|
||||
|
||||
def action_reset_expense_sheets(self):
|
||||
self._do_reverse_moves()
|
||||
self._do_reset_approval()
|
||||
|
||||
def action_register_payment(self):
|
||||
''' Open the account.payment.register wizard to pay the selected journal entries.
|
||||
There can be more than one bank_account_id in the expense sheet when registering payment for multiple expenses.
|
||||
The default_partner_bank_id is set only if there is one available, if more than one the field is left empty.
|
||||
:return: An action opening the account.payment.register wizard.
|
||||
'''
|
||||
return self.account_move_ids.with_context(default_partner_bank_id=(
|
||||
self.employee_id.sudo().bank_account_id.id if len(self.employee_id.sudo().bank_account_id.ids) <= 1 else None
|
||||
)).action_register_payment()
|
||||
|
||||
def action_open_expense_view(self):
|
||||
self.ensure_one()
|
||||
if self.nb_expense == 1:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'hr.expense',
|
||||
'res_id': self.expense_line_ids.id,
|
||||
}
|
||||
return {
|
||||
'name': _('Expenses'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'list,form',
|
||||
'views': [[False, "list"], [False, "form"]],
|
||||
'res_model': 'hr.expense',
|
||||
'domain': [('id', 'in', self.expense_line_ids.ids)],
|
||||
}
|
||||
|
||||
def action_open_account_moves(self):
|
||||
self.ensure_one()
|
||||
if self.payment_mode == 'own_account':
|
||||
res_model = 'account.move'
|
||||
record_ids = self.account_move_ids
|
||||
else:
|
||||
res_model = 'account.payment'
|
||||
record_ids = self.account_move_ids.mapped('payment_id')
|
||||
|
||||
action = {'type': 'ir.actions.act_window', 'res_model': res_model}
|
||||
if len(self.account_move_ids) == 1:
|
||||
action.update({
|
||||
'name': record_ids.name,
|
||||
'view_mode': 'form',
|
||||
'res_id': record_ids.id,
|
||||
'views': [(False, 'form')],
|
||||
})
|
||||
else:
|
||||
action.update({
|
||||
'name': _("Journal entries"),
|
||||
'view_mode': 'list',
|
||||
'domain': [('id', 'in', record_ids.ids)],
|
||||
'views': [(False, 'list'), (False, 'form')],
|
||||
})
|
||||
return action
|
||||
|
||||
# --------------------------------------------
|
||||
# Business
|
||||
# --------------------------------------------
|
||||
|
||||
def set_to_paid(self):
|
||||
# hook used in other modules to bypass payment registration
|
||||
self.write({'state': 'done'})
|
||||
|
||||
def set_to_posted(self):
|
||||
# hook used in other modules to bypass move creation
|
||||
self.write({'state': 'post'})
|
||||
|
||||
def _check_can_approve(self):
|
||||
if not all(self.mapped('can_approve')):
|
||||
reasons = _("You cannot approve:\n %s", "\n".join(self.mapped('cannot_approve_reason')))
|
||||
raise UserError(reasons)
|
||||
|
||||
def _check_can_refuse(self):
|
||||
if not all(self.mapped('can_approve')):
|
||||
reasons = _("You cannot refuse:\n %s", "\n".join(self.mapped('cannot_approve_reason')))
|
||||
raise UserError(reasons)
|
||||
|
||||
def _check_can_reset_approval(self):
|
||||
if not all(self.mapped('can_reset')):
|
||||
raise UserError(_("Only HR Officers or the concerned employee can reset to draft."))
|
||||
|
||||
def _check_can_create_move(self):
|
||||
if any(sheet.state != 'approve' for sheet in self):
|
||||
raise UserError(_("You can only generate accounting entry for approved expense(s)."))
|
||||
|
||||
if any(not sheet.journal_id for sheet in self):
|
||||
raise UserError(_("Specify expense journal to generate accounting entries."))
|
||||
|
||||
if not self.employee_id.work_email:
|
||||
raise UserError(_("The work email of the employee is required to post the expense report. Please add it on the employee form."))
|
||||
|
||||
def _do_submit(self):
|
||||
self.write({'approval_state': 'submit'})
|
||||
self.sudo().activity_update()
|
||||
|
||||
def _do_approve(self):
|
||||
for sheet in self.filtered(lambda s: s.state in {'submit', 'draft'}):
|
||||
sheet.write({
|
||||
'approval_state': 'approve',
|
||||
'user_id': sheet.user_id.id or self.env.user.id,
|
||||
'approval_date': fields.Date.context_today(sheet),
|
||||
})
|
||||
self.activity_update()
|
||||
|
||||
def _do_reset_approval(self):
|
||||
self.sudo().write({'approval_state': False})
|
||||
self.activity_update()
|
||||
|
||||
def _do_refuse(self, reason):
|
||||
self.write({'state': 'cancel'})
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
|
||||
for sheet in self:
|
||||
sheet.message_post_with_source(
|
||||
'hr_expense.hr_expense_template_refuse_reason',
|
||||
subtype_id=subtype_id,
|
||||
render_values={'reason': reason, 'name': sheet.name},
|
||||
)
|
||||
self.activity_update()
|
||||
|
||||
def _do_create_moves(self):
|
||||
self = self.with_context(clean_context(self.env.context)) # remove default_*
|
||||
skip_context = {
|
||||
'skip_invoice_sync': True,
|
||||
'skip_invoice_line_sync': True,
|
||||
'skip_account_move_synchronization': True,
|
||||
'check_move_validity': False,
|
||||
}
|
||||
own_account_sheets = self.filtered(lambda sheet: sheet.payment_mode == 'own_account')
|
||||
company_account_sheets = self - own_account_sheets
|
||||
|
||||
moves = self.env['account.move'].create([sheet._prepare_bills_vals() for sheet in own_account_sheets])
|
||||
payments = self.env['account.payment'].with_context(**skip_context).create([
|
||||
expense._prepare_payments_vals() for expense in company_account_sheets.expense_line_ids
|
||||
])
|
||||
moves |= payments.move_id
|
||||
moves.action_post()
|
||||
self.activity_update()
|
||||
|
||||
return moves
|
||||
|
||||
def _do_reverse_moves(self):
|
||||
draft_moves = self.account_move_ids.filtered(lambda account_move: account_move.state == 'draft')
|
||||
draft_moves.unlink()
|
||||
moves = self.account_move_ids - draft_moves
|
||||
moves._reverse_moves(
|
||||
default_values_list=[{'invoice_date': fields.Date.context_today(move), 'ref': False} for move in moves],
|
||||
cancel=True,
|
||||
)
|
||||
|
||||
def _prepare_bills_vals(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
**self._prepare_move_vals(),
|
||||
'invoice_date': self.accounting_date or fields.Date.context_today(self),
|
||||
'journal_id': self.journal_id.id,
|
||||
'ref': self.name,
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.employee_id.sudo().work_contact_id.id,
|
||||
'currency_id': self.currency_id.id,
|
||||
'line_ids': [Command.create(expense._prepare_move_lines_vals()) for expense in self.expense_line_ids],
|
||||
'attachment_ids': [
|
||||
Command.create(attachment.copy_data({'res_model': 'account.move', 'res_id': False, 'raw': attachment.raw})[0])
|
||||
for attachment in self.expense_line_ids.message_main_attachment_id
|
||||
],
|
||||
}
|
||||
|
||||
def _prepare_move_vals(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
# force the name to the default value, to avoid an eventual 'default_name' in the context
|
||||
# to set it to '' which cause no number to be given to the account.move when posted.
|
||||
'name': '/',
|
||||
'date': self.accounting_date or max(self.expense_line_ids.mapped('date')) or fields.Date.context_today(self),
|
||||
'expense_sheet_id': self.id,
|
||||
}
|
||||
|
||||
def _validate_analytic_distribution(self):
|
||||
for line in self.expense_line_ids:
|
||||
line._validate_distribution(account=line.account_id.id, business_domain='expense', company_id=line.company_id.id)
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
if self.user_id:
|
||||
return self.user_id
|
||||
if self.employee_id.parent_id.user_id:
|
||||
return self.employee_id.parent_id.user_id
|
||||
if self.employee_id.department_id.manager_id.user_id:
|
||||
return self.employee_id.department_id.manager_id.user_id
|
||||
return self.env['res.users']
|
||||
|
||||
def _get_expense_account_destination(self):
|
||||
self.ensure_one()
|
||||
if self.payment_mode == 'company_account':
|
||||
journal = self.payment_method_line_id.journal_id
|
||||
account_dest = (
|
||||
journal.outbound_payment_method_line_ids[:1].payment_account_id
|
||||
or journal.company_id.account_journal_payment_credit_account_id
|
||||
)
|
||||
else:
|
||||
if not self.employee_id.sudo().work_contact_id:
|
||||
raise UserError(_("No work contact found for the employee %s, please configure one.", self.employee_id.name))
|
||||
partner = self.employee_id.sudo().work_contact_id.with_company(self.company_id)
|
||||
account_dest = partner.property_account_payable_id or partner.parent_id.property_account_payable_id
|
||||
return account_dest.id
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end"><span t-field="line.unit_amount"/></td>
|
||||
<td class="text-end"><span t-field="line.price_unit"/></td>
|
||||
<td class="text-end"><span t-field="line.quantity"/></td>
|
||||
<td class="text-end">
|
||||
<t t-foreach="line.tax_ids" t-as="tax">
|
||||
@@ -62,8 +62,8 @@
|
||||
<t t-if="not tax.description"><span t-field="tax.name"/></t>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="foreign_currencies" class="text-end"><span t-field="line.total_amount" t-options='{"widget": "monetary", "display_currency": line.currency_id}'/></td>
|
||||
<td class="text-end"><span t-field="line.total_amount_company" t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/></td>
|
||||
<td t-if="foreign_currencies" class="text-end"><span t-field="line.total_amount_currency" t-options='{"widget": "monetary", "display_currency": line.currency_id}'/></td>
|
||||
<td class="text-end"><span t-field="line.total_amount" t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -77,7 +77,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end"><span t-field="o.total_amount_taxes" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/></td>
|
||||
<td class="text-end"><span t-field="o.total_tax_amount" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/></td>
|
||||
</tr>
|
||||
<tr class="fw-bold">
|
||||
<td>Total</td>
|
||||
|
||||
+2
-2
@@ -7,9 +7,9 @@ class AttachmentNumber extends Component {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.attachment_number = this.props.record.data.attachment_number
|
||||
this.nb_attachment = this.props.record.data.nb_attachment
|
||||
}
|
||||
static template = "hr_expense.AttachmentNumber"
|
||||
}
|
||||
|
||||
registry.category("fields").add("attachment_number", {component: AttachmentNumber});
|
||||
registry.category("fields").add("nb_attachment", {component: AttachmentNumber});
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<t t-name="hr_expense.AttachmentNumber">
|
||||
<div>
|
||||
<span class="fa fa-paperclip pe-1 align-middle"/>
|
||||
<t t-if="attachment_number > 0" class="text-center" t-out="attachment_number"/>
|
||||
<t t-if="nb_attachment > 0" class="text-center" t-out="nb_attachment"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import Command
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
|
||||
@@ -22,7 +23,7 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
|
||||
email='expense_user_employee@example.com',
|
||||
notification_type='email',
|
||||
groups='base.group_user',
|
||||
company_ids=[(6, 0, cls.env.companies.ids)],
|
||||
company_ids=[Command.set(cls.env.companies.ids)],
|
||||
)
|
||||
cls.expense_user_manager = mail_new_test_user(
|
||||
cls.env,
|
||||
@@ -31,24 +32,15 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
|
||||
email='expense_manager_1@example.com',
|
||||
notification_type='email',
|
||||
groups='base.group_user,hr_expense.group_hr_expense_manager',
|
||||
company_ids=[(6, 0, cls.env.companies.ids)],
|
||||
company_ids=[Command.set(cls.env.companies.ids)],
|
||||
)
|
||||
|
||||
cls.expense_employee = cls.env['hr.employee'].create({
|
||||
'name': 'expense_employee',
|
||||
'user_id': cls.expense_user_employee.id,
|
||||
'work_contact_id': cls.expense_user_employee.partner_id.id,
|
||||
'address_id': cls.expense_user_employee.partner_id.id,
|
||||
})
|
||||
|
||||
cls.product_zero_cost = cls.env['product.product'].create({
|
||||
'name': 'General',
|
||||
'default_code': 'EXP_GEN',
|
||||
'standard_price': 0.0,
|
||||
'can_be_expensed': True,
|
||||
})
|
||||
|
||||
|
||||
# Allow the current accounting user to access the expenses.
|
||||
cls.env.user.groups_id |= group_expense_manager
|
||||
|
||||
@@ -63,6 +55,7 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
|
||||
'plan_id': cls.analytic_plan.id,
|
||||
})
|
||||
|
||||
# Create product without cost
|
||||
cls.product_c = cls.env['product.product'].create({
|
||||
'name': 'product_c with no cost',
|
||||
'uom_id': cls.env.ref('uom.product_uom_dozen').id,
|
||||
@@ -72,7 +65,60 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
|
||||
'taxes_id': [Command.set((cls.tax_sale_a + cls.tax_sale_b).ids)],
|
||||
'supplier_taxes_id': [Command.set((cls.tax_purchase_a + cls.tax_purchase_b).ids)],
|
||||
'can_be_expensed': True,
|
||||
'default_code': 'product_c',
|
||||
})
|
||||
|
||||
# Ensure products can be expensed.
|
||||
# Ensure Invoicing tests products can be expensed and their code is properly set.
|
||||
(cls.product_a + cls.product_b).write({'can_be_expensed': True})
|
||||
cls.product_a.default_code = 'product_a'
|
||||
cls.product_b.default_code = 'product_b'
|
||||
|
||||
cls.frozen_today = datetime(year=2022, month=1, day=25, hour=0, minute=0, second=0)
|
||||
|
||||
# create expense account
|
||||
cls.expense_account = cls.env['account.account'].create({
|
||||
'code': '610010',
|
||||
'name': 'Expense Account 1'
|
||||
})
|
||||
|
||||
def create_expense_report(self, values=None):
|
||||
values = values or {}
|
||||
default_values = {
|
||||
'name': 'Test Expense Report',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'company_id': self.company_data['company'].id,
|
||||
'expense_line_ids': [Command.create({
|
||||
'employee_id': self.expense_employee.id,
|
||||
'product_id': self.product_c.id,
|
||||
'total_amount_currency': 1000.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
'date': self.frozen_today,
|
||||
'company_id': self.company_data['company'].id,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
})]
|
||||
}
|
||||
return self.env['hr.expense.sheet'].create({**default_values, **values})
|
||||
|
||||
def create_expense(self, values=None):
|
||||
values = values or {}
|
||||
default_values = {
|
||||
'employee_id': self.expense_employee.id,
|
||||
'product_id': self.product_c.id,
|
||||
'total_amount_currency': 1000.00,
|
||||
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
||||
'date': self.frozen_today,
|
||||
'company_id': self.company_data['company'].id,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
}
|
||||
return self.env['hr.expense'].create({**default_values, **values})
|
||||
|
||||
def get_new_payment(self, expense_sheet, amount):
|
||||
""" Helper to create payments """
|
||||
ctx = {'active_model': 'account.move', 'active_ids': expense_sheet.account_move_ids.ids}
|
||||
with freeze_time(self.frozen_today):
|
||||
payment_register = self.env['account.payment.register'].with_context(**ctx).create({
|
||||
'amount': amount,
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'payment_method_line_id': self.inbound_payment_method_line.id,
|
||||
})
|
||||
return payment_register._create_payments()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import Command
|
||||
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tests import tagged
|
||||
@@ -15,7 +15,6 @@ class TestExpensesAccessRights(TestExpenseCommon):
|
||||
'name': 'expense_employee_2',
|
||||
'user_id': self.env.user.id,
|
||||
'work_contact_id': self.env.user.partner_id.id,
|
||||
'address_id': self.env.user.partner_id.id,
|
||||
})
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
@@ -24,95 +23,68 @@ class TestExpensesAccessRights(TestExpenseCommon):
|
||||
'employee_id': expense_employee_2.id,
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'unit_amount': 1,
|
||||
'price_unit': 1,
|
||||
})
|
||||
|
||||
def test_expense_sheet_access_rights_approve(self):
|
||||
|
||||
def test_expense_sheet_access_rights(self):
|
||||
# The expense employee is able to a create an expense sheet.
|
||||
|
||||
expense_sheet = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({
|
||||
expense_sheet_approve = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({
|
||||
'name': 'First Expense for employee',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [
|
||||
(0, 0, {
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
}),
|
||||
],
|
||||
'expense_line_ids': [Command.create({
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
})],
|
||||
})
|
||||
self.env.flush_all()
|
||||
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'draft'}])
|
||||
expense_sheet_refuse = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({
|
||||
'name': 'First Expense for employee',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [Command.create({
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
})],
|
||||
})
|
||||
sheets = expense_sheet_approve | expense_sheet_refuse
|
||||
|
||||
self.assertRecordValues(sheets, [{'state': 'draft'}, {'state': 'draft'}])
|
||||
|
||||
# The expense employee is able to submit the expense sheet.
|
||||
|
||||
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
|
||||
sheets.with_user(self.expense_user_employee).action_submit_sheet()
|
||||
self.assertRecordValues(sheets, [{'state': 'submit'}, {'state': 'submit'}])
|
||||
|
||||
# The expense employee is not able to approve itself the expense sheet.
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet_approve.with_user(self.expense_user_employee).action_approve_expense_sheets()
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet.with_user(self.expense_user_employee).action_approve_expense_sheets()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
|
||||
expense_sheet_refuse.with_user(self.expense_user_employee).action_refuse_expense_sheets()
|
||||
self.assertRecordValues(sheets, [{'state': 'submit'}, {'state': 'submit'}])
|
||||
|
||||
# An expense manager is required for this step.
|
||||
|
||||
expense_sheet.with_user(self.expense_user_manager).action_approve_expense_sheets()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'approve'}])
|
||||
expense_sheet_approve.with_user(self.expense_user_manager).action_approve_expense_sheets()
|
||||
expense_sheet_refuse.with_user(self.expense_user_manager).action_refuse_expense_sheets()
|
||||
expense_sheet_refuse.with_user(self.expense_user_manager)._do_refuse('failed')
|
||||
self.assertRecordValues(sheets, [{'state': 'approve'}, {'state': 'cancel'}])
|
||||
|
||||
# An expense manager is not able to create the journal entry.
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
expense_sheet.with_user(self.expense_user_manager).action_sheet_move_create()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'approve'}])
|
||||
expense_sheet_approve.with_user(self.expense_user_manager).action_sheet_move_create()
|
||||
self.assertRecordValues(expense_sheet_approve, [{'state': 'approve'}])
|
||||
|
||||
# An expense manager having accounting access rights is able to create the journal entry.
|
||||
|
||||
expense_sheet.with_user(self.env.user).action_sheet_move_create()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'post'}])
|
||||
|
||||
def test_expense_sheet_access_rights_refuse(self):
|
||||
|
||||
# The expense employee is able to a create an expense sheet.
|
||||
|
||||
expense_sheet = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({
|
||||
'name': 'First Expense for employee',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [
|
||||
(0, 0, {
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'draft'}])
|
||||
|
||||
# The expense employee is able to submit the expense sheet.
|
||||
|
||||
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
|
||||
|
||||
# The expense employee is not able to refuse itself the expense sheet.
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet.with_user(self.expense_user_employee).action_refuse_expense_sheets()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
|
||||
|
||||
# An expense manager is required for this step.
|
||||
|
||||
expense_sheet.with_user(self.expense_user_manager).action_refuse_expense_sheets()
|
||||
expense_sheet.with_user(self.expense_user_manager)._do_refuse('failed')
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'cancel'}])
|
||||
expense_sheet_approve.with_user(self.env.user).action_sheet_move_create()
|
||||
self.assertRecordValues(expense_sheet_approve, [{'state': 'post'}])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
||||
from odoo.tests import tagged
|
||||
@@ -6,85 +5,51 @@ from odoo.tests import tagged
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestExpensesMailImport(TestExpenseCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.product_a.default_code = 'product_a'
|
||||
cls.product_b.default_code = 'product_b'
|
||||
|
||||
def test_import_expense_from_email(self):
|
||||
message_parsed = {
|
||||
'message_id': "the-world-is-a-ghetto",
|
||||
'subject': '%s %s' % (self.product_a.default_code, self.product_a.standard_price),
|
||||
'email_from': self.expense_user_employee.email,
|
||||
'to': 'catchall@yourcompany.com',
|
||||
'body': "Don't you know, that for me, and for you",
|
||||
'attachments': [],
|
||||
}
|
||||
# pylint: disable=bad-whitespace
|
||||
messages = ({
|
||||
'message_id': "the-world-is-a-ghetto",
|
||||
'subject': f'{self.product_a.default_code} {self.product_a.standard_price}',
|
||||
'email_from': self.expense_user_employee.email,
|
||||
'to': 'catchall@yourcompany.com',
|
||||
'body': "Don't you know, that for me, and for you",
|
||||
'attachments': [],
|
||||
}, {
|
||||
'message_id': "the-world-is-a-ghetto",
|
||||
'subject': 'no product code 800',
|
||||
'email_from': self.expense_user_employee.email,
|
||||
'to': 'catchall@yourcompany.com',
|
||||
'body': "Don't you know, that for me, and for you",
|
||||
'attachments': [],
|
||||
}, {
|
||||
'message_id': "test",
|
||||
'subject': 'product_c my description 100',
|
||||
'email_from': self.expense_user_employee.email,
|
||||
'to': 'catchall@yourcompany.com',
|
||||
'body': "test",
|
||||
'attachments': [],
|
||||
}
|
||||
)
|
||||
expenses = self.env['hr.expense']
|
||||
for message in messages:
|
||||
expenses |= self.env['hr.expense'].message_new(message)
|
||||
|
||||
expense = self.env['hr.expense'].message_new(message_parsed)
|
||||
self.assertRecordValues(expense, [{
|
||||
'product_id': self.product_a.id,
|
||||
'total_amount': 800.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
}])
|
||||
|
||||
def test_import_expense_from_email_no_product(self):
|
||||
message_parsed = {
|
||||
'message_id': "the-world-is-a-ghetto",
|
||||
'subject': 'no product code 800',
|
||||
'email_from': self.expense_user_employee.email,
|
||||
'to': 'catchall@yourcompany.com',
|
||||
'body': "Don't you know, that for me, and for you",
|
||||
'attachments': [],
|
||||
}
|
||||
|
||||
expense = self.env['hr.expense'].message_new(message_parsed)
|
||||
|
||||
self.assertRecordValues(expense, [{
|
||||
'product_id': False,
|
||||
'total_amount': 800.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
}])
|
||||
|
||||
def test_import_expense_from_email_product_no_cost(self):
|
||||
"""
|
||||
We have to compute a value for the total amount
|
||||
even if the product has no cost.
|
||||
"""
|
||||
product_no_cost = self.env['product.product'].create({
|
||||
'name': 'Product No Cost',
|
||||
'standard_price': 0.0,
|
||||
'can_be_expensed': True,
|
||||
'default_code': 'product_no_cost',
|
||||
})
|
||||
message_parsed = {
|
||||
'message_id': "test",
|
||||
'subject': 'product_no_cost my description 100',
|
||||
'email_from': self.expense_user_employee.email,
|
||||
'to': 'catchall@yourcompany.com',
|
||||
'body': "test",
|
||||
'attachments': [],
|
||||
}
|
||||
expense = self.env['hr.expense'].message_new(message_parsed)
|
||||
self.assertRecordValues(expense, [{
|
||||
'product_id': product_no_cost.id,
|
||||
'total_amount': 100.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
}])
|
||||
self.assertRecordValues(expenses, [
|
||||
{'product_id': self.product_a.id, 'total_amount_currency': 800.0, 'employee_id': self.expense_employee.id},
|
||||
{'product_id': False, 'total_amount_currency': 800.0, 'employee_id': self.expense_employee.id},
|
||||
{'product_id': self.product_c.id, 'total_amount_currency': 100.0, 'employee_id': self.expense_employee.id},
|
||||
])
|
||||
|
||||
def test_import_expense_from_mail_parsing_subjects(self):
|
||||
|
||||
def assertParsedValues(subject, currencies, exp_description, exp_amount, exp_product):
|
||||
product, amount, currency_id, description = self.env['hr.expense']\
|
||||
.with_user(self.expense_user_employee)\
|
||||
def assertParsedValues(subject, currencies, exp_description, exp_amount, exp_product, exp_currency):
|
||||
product, amount, currency_id, description = self.env['hr.expense'] \
|
||||
.with_user(self.expense_user_employee) \
|
||||
._parse_expense_subject(subject, currencies)
|
||||
|
||||
self.assertEqual(product, exp_product)
|
||||
self.assertAlmostEqual(amount, exp_amount)
|
||||
self.assertEqual(description, exp_description)
|
||||
self.assertEqual(currency_id, exp_currency)
|
||||
|
||||
# Without Multi currency access
|
||||
assertParsedValues(
|
||||
@@ -93,35 +58,37 @@ class TestExpensesMailImport(TestExpenseCommon):
|
||||
"bar electro wizard",
|
||||
1205.91,
|
||||
self.product_a,
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
# subject having other currency then company currency, it should ignore other currency then company currency
|
||||
assertParsedValues(
|
||||
"foo bar %s1406.91 royal giant" % self.currency_data['currency'].symbol,
|
||||
f'foo bar {self.currency_data["currency"].symbol}1406.91 royal giant',
|
||||
self.company_data['currency'],
|
||||
"foo bar %s royal giant" % self.currency_data['currency'].symbol,
|
||||
f'foo bar {self.currency_data["currency"].symbol} royal giant',
|
||||
1406.91,
|
||||
self.env['product.product'],
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
# With Multi currency access
|
||||
self.expense_user_employee.groups_id |= self.env.ref('base.group_multi_currency')
|
||||
|
||||
assertParsedValues(
|
||||
"product_a foo bar $2205.92 elite barbarians",
|
||||
self.company_data['currency'],
|
||||
"foo bar elite barbarians",
|
||||
2205.92,
|
||||
self.product_a,
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
# subject having other currency then company currency, it should accept other currency because multi currency is activated
|
||||
assertParsedValues(
|
||||
"product_a %s2510.90 chhota bheem" % self.currency_data['currency'].symbol,
|
||||
f'product_a {self.currency_data["currency"].symbol}2510.90 chhota bheem',
|
||||
self.company_data['currency'] + self.currency_data['currency'],
|
||||
"chhota bheem",
|
||||
2510.90,
|
||||
self.product_a,
|
||||
self.currency_data['currency'],
|
||||
)
|
||||
|
||||
# subject without product and currency, should take company currency and default product
|
||||
@@ -131,6 +98,7 @@ class TestExpensesMailImport(TestExpenseCommon):
|
||||
"foo bar spear goblins",
|
||||
109.96,
|
||||
self.env['product.product'],
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
# subject with currency symbol at end
|
||||
@@ -140,6 +108,7 @@ class TestExpensesMailImport(TestExpenseCommon):
|
||||
"foo bar inferno dragon",
|
||||
2910.94,
|
||||
self.product_a,
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
# subject with no amount and product
|
||||
@@ -149,6 +118,7 @@ class TestExpensesMailImport(TestExpenseCommon):
|
||||
"foo bar mega knight",
|
||||
0.0,
|
||||
self.env['product.product'],
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
# price with a comma
|
||||
@@ -158,21 +128,23 @@ class TestExpensesMailImport(TestExpenseCommon):
|
||||
"foo bar mega knight",
|
||||
291.56,
|
||||
self.env['product.product'],
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
# price without decimals
|
||||
# price different decimals than currency
|
||||
assertParsedValues(
|
||||
"foo bar 291$ mega knight",
|
||||
self.company_data['currency'] + self.currency_data['currency'],
|
||||
"foo bar mega knight",
|
||||
291.0,
|
||||
self.env['product.product'],
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
assertParsedValues(
|
||||
"product_a foo bar 291.5$ mega knight",
|
||||
self.company_data['currency'] + self.currency_data['currency'],
|
||||
"foo bar mega knight",
|
||||
291.5,
|
||||
self.product_a,
|
||||
self.company_data['currency'],
|
||||
)
|
||||
|
||||
@@ -1,118 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import Command
|
||||
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestExpenseMultiCompany(TestExpenseCommon):
|
||||
|
||||
def test_expense_sheet_multi_company_approve(self):
|
||||
def test_expense_sheet_multi_company(self):
|
||||
self.expense_employee.company_id = self.company_data_2['company']
|
||||
|
||||
# The expense employee is able to a create an expense sheet for company_2.
|
||||
# product_a needs a standard_price in company_2
|
||||
self.product_a.with_context(allowed_company_ids=self.company_data_2['company'].ids).standard_price = 100
|
||||
|
||||
expense_sheet = self.env['hr.expense.sheet']\
|
||||
.with_user(self.expense_user_employee)\
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
|
||||
expense_sheet_approve = self.env['hr.expense.sheet'] \
|
||||
.with_user(self.expense_user_employee) \
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids) \
|
||||
.create({
|
||||
'name': 'First Expense for employee',
|
||||
'name': 'First Expense for employee',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data_2['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [Command.create({
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data_2['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [
|
||||
(0, 0, {
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assertRecordValues(expense_sheet, [{'company_id': self.company_data_2['company'].id}])
|
||||
})],
|
||||
})
|
||||
expense_sheet_refuse = self.env['hr.expense.sheet'] \
|
||||
.with_user(self.expense_user_employee) \
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids) \
|
||||
.create({
|
||||
'name': 'First Expense for employee',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data_2['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [Command.create({
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
})],
|
||||
})
|
||||
expenses = expense_sheet_approve | expense_sheet_refuse
|
||||
self.assertRecordValues(expenses, [
|
||||
{'company_id': self.company_data_2['company'].id},
|
||||
{'company_id': self.company_data_2['company'].id},
|
||||
])
|
||||
|
||||
# The expense employee is able to submit the expense sheet.
|
||||
expenses.with_user(self.expense_user_employee).action_submit_sheet()
|
||||
|
||||
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
|
||||
|
||||
# An expense manager is not able to approve without access to company_2.
|
||||
|
||||
# An expense manager is not able to approve nor refuse without access to company_2.
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet\
|
||||
.with_user(self.expense_user_manager)\
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids)\
|
||||
expense_sheet_approve \
|
||||
.with_user(self.expense_user_manager) \
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids) \
|
||||
.action_approve_expense_sheets()
|
||||
|
||||
# An expense manager is able to approve with access to company_2.
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet_refuse \
|
||||
.with_user(self.expense_user_manager) \
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids) \
|
||||
._do_refuse('failed')
|
||||
|
||||
expense_sheet\
|
||||
.with_user(self.expense_user_manager)\
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
|
||||
# An expense manager is able to approve/refuse with access to company_2.
|
||||
expense_sheet_approve \
|
||||
.with_user(self.expense_user_manager) \
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids) \
|
||||
.action_approve_expense_sheets()
|
||||
expense_sheet_refuse \
|
||||
.with_user(self.expense_user_manager) \
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids) \
|
||||
._do_refuse('failed')
|
||||
|
||||
# An expense manager having accounting access rights is not able to create the journal entry without access
|
||||
# to company_2.
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet\
|
||||
.with_user(self.env.user)\
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids)\
|
||||
expense_sheet_approve \
|
||||
.with_user(self.env.user) \
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids) \
|
||||
.action_sheet_move_create()
|
||||
|
||||
# An expense manager having accounting access rights is able to create the journal entry with access to
|
||||
# company_2.
|
||||
|
||||
expense_sheet\
|
||||
.with_user(self.env.user)\
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
|
||||
expense_sheet_approve \
|
||||
.with_user(self.env.user) \
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids) \
|
||||
.action_sheet_move_create()
|
||||
|
||||
def test_expense_sheet_multi_company_refuse(self):
|
||||
self.expense_employee.company_id = self.company_data_2['company']
|
||||
|
||||
# The expense employee is able to a create an expense sheet for company_2.
|
||||
|
||||
expense_sheet = self.env['hr.expense.sheet']\
|
||||
.with_user(self.expense_user_employee)\
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
|
||||
.create({
|
||||
'name': 'First Expense for employee',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data_2['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [
|
||||
(0, 0, {
|
||||
# Expense without foreign currency but analytic account.
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 1000.0,
|
||||
'employee_id': self.expense_employee.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
self.assertRecordValues(expense_sheet, [{'company_id': self.company_data_2['company'].id}])
|
||||
|
||||
# The expense employee is able to submit the expense sheet.
|
||||
|
||||
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
|
||||
|
||||
# An expense manager is not able to approve without access to company_2.
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet\
|
||||
.with_user(self.expense_user_manager)\
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids)\
|
||||
._do_refuse('failed')
|
||||
|
||||
# An expense manager is able to approve with access to company_2.
|
||||
|
||||
expense_sheet\
|
||||
.with_user(self.expense_user_manager)\
|
||||
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
|
||||
._do_refuse('failed')
|
||||
|
||||
@@ -20,8 +20,8 @@ class TestExpensesTax(TestExpenseCommon):
|
||||
self.env['hr.expense'].create({
|
||||
'name': 'Test Tax Used',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 350.00,
|
||||
'product_id': self.product_c.id,
|
||||
'total_amount_currency': 350.00,
|
||||
'tax_ids': [Command.set(tax_expense.ids)]
|
||||
})
|
||||
tax_expense.invalidate_model(fnames=['is_used'])
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('o_kanban_manage_reports')]" position="inside">
|
||||
<a role="menuitem" class="dropdown-item" name="%(action_hr_expense_sheet_department_filtered)d" type="action" groups="hr_expense.group_hr_expense_team_approver">
|
||||
<a role="menuitem" class="dropdown-item" name="%(action_hr_expense_sheet_department_filtered)d"
|
||||
type="action" groups="hr_expense.group_hr_expense_team_approver">
|
||||
Expenses
|
||||
</a>
|
||||
</xpath>
|
||||
|
||||
@@ -45,43 +45,48 @@
|
||||
<field name="model">hr.expense</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Expenses" multi_edit="1" sample="1" js_class="hr_expense_tree" decoration-info="state == 'draft'">
|
||||
<field name="is_editable" column_invisible="True"/>
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
<field name="company_currency_id" column_invisible="True"/>
|
||||
<field name="attachment_number" column_invisible="True"/>
|
||||
<field name="is_editable" column_invisible="True"/>
|
||||
<field name="sheet_is_editable" column_invisible="True"/>
|
||||
<field name="date" optional="show" readonly="state == 'done'"/>
|
||||
<field name="product_id" optional="hide" readonly="not sheet_is_editable"/>
|
||||
<field name="name" readonly="not sheet_is_editable"/>
|
||||
<field name="employee_id" widget="many2one_avatar_user" readonly="not sheet_is_editable"/>
|
||||
<field name="sheet_id" optional="show" readonly="1" column_invisible="not context.get('show_report', False)"/>
|
||||
<field name="payment_mode" optional="show" readonly="not sheet_is_editable"/>
|
||||
<field name="nb_attachment" column_invisible="True"/>
|
||||
<field name="is_multiple_currency" column_invisible="True"/>
|
||||
<field name="product_has_cost" column_invisible="True"/>
|
||||
<field name="date" optional="show" readonly="not is_editable"/>
|
||||
<field name="product_id" optional="hide" readonly="not is_editable"/>
|
||||
<field name="name" readonly="not is_editable"/>
|
||||
<field name="employee_id" widget="many2one_avatar_user" readonly="not is_editable"/>
|
||||
<field name="sheet_id" optional="show" readonly="True" column_invisible="not context.get('show_report', False)"/>
|
||||
<field name="payment_mode" optional="show" readonly="not is_editable"/>
|
||||
<field name="activity_ids" widget="list_activity" optional="show"/>
|
||||
<field name="accounting_date" optional="hide" groups="account.group_account_invoice,account.group_account_readonly"
|
||||
readonly="not sheet_is_editable"/>
|
||||
<field name="reference" optional="hide" groups="account.group_account_readonly" readonly="not sheet_is_editable"/>
|
||||
readonly="not is_editable"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
optional="show"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
readonly="not sheet_is_editable"
|
||||
readonly="not is_editable"
|
||||
options="{'product_field': 'product_id', 'business_domain': 'expense'}"/>
|
||||
<field name="account_id" optional="hide" groups="account.group_account_readonly"
|
||||
readonly="not sheet_is_editable"/>
|
||||
<field name="company_id" optional="show" groups="base.group_multi_company" readonly="1"/>
|
||||
<field name="unit_amount" string="Unit Price" optional="hide" widget="monetary" readonly="True"
|
||||
options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="quantity" optional="hide" readonly="not sheet_is_editable"/>
|
||||
readonly="not is_editable"/>
|
||||
<field name="company_id" optional="show" groups="base.group_multi_company" readonly="True"/>
|
||||
<field name="price_unit" string="Unit Price" optional="hide" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}" readonly="True"/>
|
||||
<field name="quantity" optional="hide" readonly="not is_editable or not product_has_cost"/>
|
||||
<field name="tax_ids" optional="hide" widget="many2many_tags"
|
||||
groups="account.group_account_invoice,account.group_account_readonly"
|
||||
readonly="not sheet_is_editable"/>
|
||||
<field name="amount_tax_company" sum="Total Taxes"
|
||||
readonly="not is_editable or not product_has_cost"/>
|
||||
<field name="tax_amount" sum="Total Taxes" readonly="True"
|
||||
optional="hide" groups="account.group_account_invoice,account.group_account_readonly"/>
|
||||
<field name="attachment_number" widget="attachment_number" nolabel="1"/>
|
||||
<field name="total_amount_company" sum="Total Amount" widget='monetary' readonly="not sheet_is_editable"
|
||||
<field name="nb_attachment" widget="nb_attachment" nolabel="1" readonly="True"/>
|
||||
<field name="total_amount" sum="Total Amount" widget='monetary'
|
||||
readonly="not is_editable or product_has_cost"
|
||||
options="{'currency_field': 'company_currency_id'}" decoration-bf="1"/>
|
||||
<field name="total_amount" widget='monetary' readonly="not sheet_is_editable" options="{'currency_field': 'currency_id'}" optional="hide" decoration-bf="1" groups="base.group_multi_currency"/>
|
||||
<field name="total_amount_currency" widget='monetary'
|
||||
readonly="not is_editable or not is_multiple_currency or product_has_cost"
|
||||
options="{'currency_field': 'currency_id'}" optional="hide" decoration-bf="1"
|
||||
groups="base.group_multi_currency"/>
|
||||
<field name="currency_id" optional="hide" readonly="True" groups="base.group_multi_currency"/>
|
||||
<field name="state" optional="show" readonly="1" decoration-info="state in ['draft', 'reported']" decoration-success="state in ['approved', 'done']"
|
||||
<field name="state" optional="show" readonly="True" decoration-info="state in ['draft', 'reported']"
|
||||
decoration-success="state in ['approved', 'done']"
|
||||
decoration-warning="state == 'submitted'" decoration-danger="state == 'refused'" widget="badge"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -120,10 +125,12 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Expenses" js_class="hr_expense_form_view">
|
||||
<header>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="oe_highlight o_expense_submit" invisible="attachment_number <= 0 or sheet_id" data-hotkey="v"/>
|
||||
<widget name="attach_document" string="Attach Receipt" action="attach_document" invisible="attachment_number < 1"/>
|
||||
<widget name="attach_document" string="Attach Receipt" action="attach_document" highlight="1" invisible="attachment_number >= 1"/>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="o_expense_submit" invisible="attachment_number >= 1 or sheet_id" data-hotkey="v"/>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object"
|
||||
class="oe_highlight o_expense_submit" invisible="nb_attachment <= 0 or sheet_id" data-hotkey="v"/>
|
||||
<widget name="attach_document" string="Attach Receipt" action="attach_document" invisible="nb_attachment < 1"/>
|
||||
<widget name="attach_document" string="Attach Receipt" action="attach_document" highlight="1" invisible="nb_attachment >= 1"/>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="o_expense_submit"
|
||||
invisible="nb_attachment >= 1 or sheet_id" data-hotkey="v"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,reported,submitted,approved,done"
|
||||
invisible="state == 'refused'"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,reported,submitted,refused"
|
||||
@@ -135,61 +142,67 @@
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="e.g. Lunch with Customer" readonly="not sheet_is_editable"/>
|
||||
<field name="name" placeholder="e.g. Lunch with Customer" readonly="not is_editable"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="sheet_id" invisible="1"/>
|
||||
<field name="product_has_cost" invisible="1"/>
|
||||
<field name="product_has_tax" invisible="1"/>
|
||||
<field name="same_currency" invisible="1"/>
|
||||
<field name="is_multiple_currency" invisible="1"/>
|
||||
<field name="is_editable" invisible="1"/>
|
||||
<field name="is_ref_editable" invisible="1"/>
|
||||
<field name="currency_id" invisible="1" readonly="state in ['approved', 'done', 'reported']"/>
|
||||
<field name="company_id" invisible="1" readonly="state not in ['draft', 'refused']"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="amount_tax_company" invisible="1"/>
|
||||
<field name="unit_amount" invisible="1" readonly="state == 'done'"/>
|
||||
<field name="attachment_number" invisible="1"/>
|
||||
<field name="total_amount_company" invisible="1"/>
|
||||
<field name="tax_amount" invisible="1"/>
|
||||
<field name="price_unit" invisible="1"/>
|
||||
<field name="nb_attachment" invisible="1"/>
|
||||
<field name="total_amount" invisible="1"/>
|
||||
<field name="duplicate_expense_ids" invisible="1"/>
|
||||
<field name="sheet_is_editable" invisible="1"/>
|
||||
<field name="currency_rate" invisible="1"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
|
||||
<label for="product_id"/>
|
||||
<div>
|
||||
<field name="product_id" required="1" readonly="not sheet_is_editable" context="{'default_can_be_expensed': 1, 'tree_view_ref': 'hr_expense.product_product_expense_tree_view', 'form_view_ref': 'hr_expense.product_product_expense_form_view'}" class="w-100"/>
|
||||
<div class="fst-italic" invisible="not product_description or not product_id">
|
||||
<field name="product_id" required="1" readonly="not is_editable"
|
||||
context="{'default_can_be_expensed': 1, 'tree_view_ref': 'hr_expense.product_product_expense_tree_view', 'form_view_ref': 'hr_expense.product_product_expense_form_view'}"
|
||||
class="w-100"/>
|
||||
<div class="fst-italic" invisible="not is_editable or not product_description or not product_id">
|
||||
<field name="product_description"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CASE: product has a cost defined -> user input qty (always in company currency) -->
|
||||
<field name="unit_amount" force_save="1" widget="monetary" options="{'currency_field': 'currency_id', 'field_digits': True}" required="1" invisible="not product_has_cost" readonly="not sheet_is_editable or product_has_cost"/>
|
||||
<field name="price_unit" required="1" widget="monetary"
|
||||
options="{'currency_field': 'currency_id', 'field_digits': True}"
|
||||
invisible="not product_has_cost" readonly="True"/>
|
||||
<label for="quantity" invisible="not product_has_cost"/>
|
||||
<div invisible="not product_has_cost">
|
||||
<div class="o_row">
|
||||
<field name="quantity" class="oe_inline" readonly="not sheet_is_editable"/>
|
||||
<field name="product_uom_id" required="1" force_save="1" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"
|
||||
readonly="not sheet_is_editable"/>
|
||||
<field name="quantity" class="oe_inline" readonly="not is_editable"/>
|
||||
<field name="product_uom_id" required="1" force_save="1"
|
||||
options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"
|
||||
readonly="not is_editable"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CASE: product has no cost defined -> user input amount (in other currency if multi-currency) -->
|
||||
<label for="total_amount" string="Total" invisible="product_has_cost"/>
|
||||
<label for="total_amount_currency" string="Total" invisible="product_has_cost" readonly="not is_editable"/>
|
||||
<div class="o_row" invisible="product_has_cost">
|
||||
<field name="total_amount" widget='monetary' options="{'currency_field': 'currency_id'}" readonly="not sheet_is_editable" class="oe_inline mw-50 me-0"/>
|
||||
<field name="currency_id" class="mw-25 ms-0" groups="base.group_multi_currency" options="{'no_create': True}"
|
||||
readonly="not sheet_is_editable"/>
|
||||
<field name="total_amount_currency" widget='monetary' options="{'currency_field': 'currency_id'}"
|
||||
readonly="not is_editable" class="oe_inline mw-50 me-0"/>
|
||||
<field name="currency_id" class="mw-25 ms-0" groups="base.group_multi_currency"
|
||||
options="{'no_create': True}"
|
||||
readonly="not is_editable"/>
|
||||
</div>
|
||||
|
||||
<!-- CASE: converter when currency is different than the company one -->
|
||||
<label for="total_amount_company" string="" invisible="same_currency and not product_has_cost"/>
|
||||
<div class="o_row" invisible="same_currency and not product_has_cost">
|
||||
<field name="total_amount_company" widget='monetary' options="{'currency_field': 'company_currency_id'}"
|
||||
readonly="not sheet_is_editable" class="oe_inline"/>
|
||||
<field name="label_convert_rate" class="ps-0"/>
|
||||
<label for="total_amount" string="" invisible="not is_multiple_currency and not product_has_cost"/>
|
||||
<div class="o_row" invisible="not is_multiple_currency and not product_has_cost">
|
||||
<field name="total_amount" widget='monetary' options="{'currency_field': 'company_currency_id'}"
|
||||
force_save="1" readonly="not is_editable or product_has_cost" class="oe_inline"/>
|
||||
<field name="label_currency_rate" class="ps-0"/>
|
||||
</div>
|
||||
|
||||
<label for="tax_ids"/>
|
||||
@@ -197,33 +210,43 @@
|
||||
<field name="tax_ids"
|
||||
force_save="1"
|
||||
widget="many2many_tags"
|
||||
readonly="not sheet_is_editable"
|
||||
readonly="not is_editable"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="amount_tax"/>
|
||||
<field name="tax_amount"/>
|
||||
</div>
|
||||
<t groups="hr_expense.group_hr_expense_team_approver">
|
||||
<field name="employee_id" groups="!hr.group_hr_user" context="{'default_company_id': company_id}" widget="many2one_avatar_employee" options="{'relation': 'hr.employee.public', 'no_create': True}" readonly="not sheet_is_editable"/>
|
||||
<field name="employee_id" groups="hr.group_hr_user" context="{'default_company_id': company_id}" widget="many2one_avatar_employee" options="{'relation': 'hr.employee', 'no_create': True}" readonly="not sheet_is_editable"/>
|
||||
<field name="employee_id" groups="!hr.group_hr_user"
|
||||
context="{'default_company_id': company_id}" widget="many2one_avatar_employee"
|
||||
options="{'relation': 'hr.employee.public', 'no_create': True}"
|
||||
readonly="not is_editable"/>
|
||||
<field name="employee_id" groups="hr.group_hr_user"
|
||||
context="{'default_company_id': company_id}" widget="many2one_avatar_employee"
|
||||
options="{'relation': 'hr.employee', 'no_create': True}"
|
||||
readonly="not is_editable"/>
|
||||
</t>
|
||||
<label id="lo" for="payment_mode" invisible="product_has_cost"/>
|
||||
<div id="payment_mode" invisible="product_has_cost">
|
||||
<label id="lo" for="payment_mode"/>
|
||||
<div id="payment_mode">
|
||||
<field name="payment_mode" widget="radio" readonly="sheet_id"/>
|
||||
</div>
|
||||
</group><group>
|
||||
<field name="reference" groups="account.group_account_readonly" invisible="product_has_cost" readonly="not is_ref_editable"/>
|
||||
<field name="date" readonly="not sheet_is_editable"/>
|
||||
<field name="accounting_date" invisible="not accounting_date or state not in ['approved', 'done']" readonly=" not sheet_is_editable"/>
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card')), ('company_id', '=', company_id)]" groups="account.group_account_readonly" readonly="not sheet_is_editable" context="{'default_company_id': company_id}"/>
|
||||
<field name="sheet_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date" readonly="not is_editable"/>
|
||||
<field name="accounting_date"
|
||||
invisible="not accounting_date or state not in ['approved', 'done']"
|
||||
readonly="not is_editable"/>
|
||||
<field name="account_id" options="{'no_create': True}"
|
||||
domain="[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card')), ('company_id', '=', company_id)]"
|
||||
groups="account.group_account_readonly" readonly="not is_editable"
|
||||
context="{'default_company_id': company_id}"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'product_field': 'product_id', 'account_field': 'account_id', 'business_domain': 'expense'}"
|
||||
readonly="not sheet_is_editable"/>
|
||||
<field name="company_id" groups="base.group_multi_company" readonly="not sheet_is_editable"/>
|
||||
readonly="not is_editable"/>
|
||||
<field name="company_id" groups="base.group_multi_company" readonly="state != 'draft'"/>
|
||||
</group>
|
||||
</group>
|
||||
<div>
|
||||
<field name="description" placeholder="Notes..." readonly="not sheet_is_editable"/>
|
||||
<field name="description" placeholder="Notes..." readonly="not is_editable"/>
|
||||
</div>
|
||||
</sheet>
|
||||
<div class="o_attachment_preview o_center_attachment"/>
|
||||
@@ -262,7 +285,7 @@
|
||||
<kanban class="o_kanban_mobile hr_expense" sample="1" js_class="hr_expense_kanban" quick_create="false">
|
||||
<field name="name"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="total_amount"/>
|
||||
<field name="total_amount_currency"/>
|
||||
<field name="date"/>
|
||||
<field name="state"/>
|
||||
<field name="activity_state"/>
|
||||
@@ -274,18 +297,24 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<strong class="o_kanban_record_title"><span><t t-out="record.name.value"/></span></strong>
|
||||
<strong class="o_kanban_record_subtitle float-end"><span class="text-end"><field name="total_amount" widget="monetary"/></span></strong>
|
||||
<strong class="o_kanban_record_subtitle float-end"><span class="text-end">
|
||||
<field name="total_amount_currency" widget="monetary"/></span>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<div class="col-6 text-muted">
|
||||
<field name="employee_id" widget="many2one_avatar_user" options="{'display_avatar_name': True}" readonly="state in ['approved', 'done']"/><br/>
|
||||
<field name="employee_id" widget="many2one_avatar_user"
|
||||
options="{'display_avatar_name': True}"
|
||||
readonly="True"/><br/>
|
||||
<t t-out="record.date.value"/>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span class="float-end text-end">
|
||||
<field name="state" widget="label_selection" options="{'classes': {'draft': 'default', 'reported': 'primary', 'submitted': 'warning', 'refused': 'danger', 'done': 'warning',
|
||||
'approved': 'success'}}"/>
|
||||
<field name="state" widget="label_selection"
|
||||
options="{'classes': {'draft': 'default', 'reported': 'primary',
|
||||
'submitted': 'warning', 'refused': 'danger',
|
||||
'done': 'warning', 'approved': 'success'}}"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,7 +358,7 @@
|
||||
<pivot string="Expenses Analysis" sample="1">
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="date" interval="month" type="col"/>
|
||||
<field name="total_amount" type="measure"/>
|
||||
<field name="total_amount_currency" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
@@ -339,12 +368,12 @@
|
||||
<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="price_unit" invisible="1"/>
|
||||
<field name="quantity" invisible="1"/>
|
||||
<field name="date"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="total_amount_company" type="measure" />
|
||||
<field name="amount_tax_company"/>
|
||||
<field name="total_amount" type="measure" />
|
||||
<field name="tax_amount"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
@@ -354,18 +383,21 @@
|
||||
<field name="model">hr.expense</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Expense">
|
||||
<field string="Expense" name="name" filter_domain="['|', '|', ('employee_id', 'ilike', self), ('name', 'ilike', self), ('product_id', 'ilike', self)]"/>
|
||||
<field string="Expense" name="name"
|
||||
filter_domain="['|', '|', ('employee_id', 'ilike', self), ('name', 'ilike', self), ('product_id', 'ilike', self)]"/>
|
||||
<field name="date"/>
|
||||
<field name="employee_id"/>
|
||||
<filter string="My Expenses" name="my_expenses" domain="[('employee_id.user_id', '=', uid)]"/>
|
||||
<filter string="My Team" name="my_team_expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="hr_expense.group_hr_expense_team_approver" help="Expenses of Your Team Member"/>
|
||||
<filter string="My Team" name="my_team_expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]"
|
||||
groups="hr_expense.group_hr_expense_team_approver" help="Expenses of Your Team Member"/>
|
||||
<separator />
|
||||
<filter string="To Report" name="no_report" domain="[('sheet_id', '=', False)]"/>
|
||||
<filter string="Refused" name="refused" domain="[('state', '=', 'refused')]" help="Refused Expenses"/>
|
||||
<separator />
|
||||
<filter string="Expense Date" name="date" date="date"/>
|
||||
<separator />
|
||||
<filter string="Former Employees" name="inactive" domain="[('employee_id.active', '=', False)]" groups="hr_expense.group_hr_expense_user,hr_expense.group_hr_expense_manager"/>
|
||||
<filter string="Former Employees" name="inactive" domain="[('employee_id.active', '=', False)]"
|
||||
groups="hr_expense.group_hr_expense_user,hr_expense.group_hr_expense_manager"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
@@ -378,8 +410,10 @@
|
||||
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
|
||||
<filter string="Category" name="product" domain="[]" context="{'group_by': 'product_id'}"/>
|
||||
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
|
||||
<filter string="Expense Date" name="expensesmonth" domain="[]" context="{'group_by': 'date'}" help="Expense Date"/>
|
||||
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
|
||||
<filter string="Expense Date" name="expensesmonth" domain="[]" context="{'group_by': 'date'}"
|
||||
help="Expense Date"/>
|
||||
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}"
|
||||
groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
@@ -394,10 +428,12 @@
|
||||
<field name="currency_id"/>
|
||||
<templates>
|
||||
<div t-name="activity-box">
|
||||
<img class="rounded" t-att-src="activity_image('hr.employee', 'avatar_128', record.employee_id.raw_value)" t-att-title="record.employee_id.value" t-att-alt="record.employee_id.value"/>
|
||||
<img class="rounded"
|
||||
t-att-src="activity_image('hr.employee', 'avatar_128', record.employee_id.raw_value)"
|
||||
t-att-title="record.employee_id.value" t-att-alt="record.employee_id.value"/>
|
||||
<div class="ms-2">
|
||||
<field name="name" display="full" class="o_text_block"/>
|
||||
<field name="total_amount" widget="monetary" muted="1" display="full"/>
|
||||
<field name="total_amount_currency" widget="monetary" muted="1" display="full"/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -458,10 +494,13 @@
|
||||
<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"/>
|
||||
<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 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>
|
||||
@@ -531,13 +570,20 @@
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="type" invisible="1"/>
|
||||
<field name="detailed_type" invisible="1"/>
|
||||
<field name="standard_price" class="w-25" help="When the cost of an expense product is different than 0, then the user using this product won't be able to change the amount of the expense, only the quantity. Use a cost different than 0 for expense categories funded by the company at fixed cost like allowances for mileage, per diem, accommodation or meal."/>
|
||||
<field name="standard_price" class="w-25"
|
||||
help="When the cost of an expense product is different than 0, then the user
|
||||
using this product won't be able to change the amount of the expense,
|
||||
only the quantity. Use a cost different than 0 for expense categories funded by
|
||||
the company at fixed cost like allowances for mileage, per diem, accommodation
|
||||
or meal."/>
|
||||
<field name="uom_id" class="w-25" groups="uom.group_uom" options="{'no_create': True}"/>
|
||||
<field name="uom_po_id" invisible="1"/>
|
||||
<label for="default_code"/>
|
||||
<div>
|
||||
<field name="default_code" class="w-50"/>
|
||||
<span class="d-inline-block"><i class="text-muted">Use this reference as a subject prefix when submitting by email.</i></span>
|
||||
<span class="d-inline-block">
|
||||
<i class="text-muted">Use this reference as a subject prefix when submitting by email.</i>
|
||||
</span>
|
||||
</div>
|
||||
<field name="categ_id" class="w-50"/>
|
||||
<field name="company_id" class="w-50" groups="base.group_multi_company"/>
|
||||
@@ -549,7 +595,8 @@
|
||||
options="{'no_quick_create': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="description" class="mt-5" placeholder="This note will be shown to users when they select this expense product."/>
|
||||
<field name="description" class="mt-5"
|
||||
placeholder="This note will be shown to users when they select this expense product."/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
@@ -577,7 +624,8 @@
|
||||
<tree string="Product Variants">
|
||||
<field name="default_code"/>
|
||||
<field name="name"/>
|
||||
<field name="product_template_attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/>
|
||||
<field name="product_template_attribute_value_ids" widget="many2many_tags"
|
||||
groups="product.group_product_variant"/>
|
||||
<field name="standard_price"/>
|
||||
<field name="uom_id" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/>
|
||||
<field name="barcode"/>
|
||||
@@ -642,24 +690,31 @@
|
||||
<field name="model">hr.expense.sheet</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Expense Reports" multi_edit="1" js_class="hr_expense_tree" sample="1" decoration-info="state == 'draft'">
|
||||
<field name="company_id" column_invisible="True" readonly="state != 'draft'"/>
|
||||
<field name="product_ids" column_invisible="True"/>
|
||||
<field name="currency_id" column_invisible="True" readonly="state != 'draft'"/>
|
||||
<field name="currency_id" column_invisible="True"/>
|
||||
<field name="company_currency_id" column_invisible="True"/>
|
||||
<field name="is_editable" column_invisible="True"/>
|
||||
<field name="employee_id" widget="many2one_avatar_user" readonly="state != 'draft'"/>
|
||||
<field name="accounting_date" optional="hide" groups="account.group_account_manager"/>
|
||||
<field name="accounting_date" optional="hide" groups="account.group_account_manager" readonly="not is_editable"/>
|
||||
<field name="create_date" optional="hide"/>
|
||||
<field name="name" string="Expense Report"/>
|
||||
<field name="name" string="Expense Report" readonly="not is_editable"/>
|
||||
<field name="payment_mode" optional="hide"/>
|
||||
<field name="user_id" optional="hide" widget="many2one_avatar_user" readonly="state != 'draft'"/>
|
||||
<field name="company_id" optional="show" groups="base.group_multi_company" readonly="state != 'draft'"/>
|
||||
<field name="activity_ids" widget="list_activity" optional="show" readonly="1"/>
|
||||
<field name="journal_id" optional="hide"/>
|
||||
<field name="activity_ids" widget="list_activity" optional="show" readonly="True"/>
|
||||
<field name="journal_id" optional="hide" readonly="not is_editable"/>
|
||||
<field name="total_amount" sum="Total Amount" decoration-bf="1" widget="monetary"/>
|
||||
<field name="state" optional="show" decoration-info="state == 'draft'" decoration-success="state in ['approve', 'post', 'done']"
|
||||
decoration-warning="state == 'submit'" decoration-danger="state == 'cancel'" widget="badge"/>
|
||||
<field name="payment_state" optional="show" decoration-info="payment_state in ('partial','in_payment')" decoration-success="payment_state == 'paid'"
|
||||
decoration-danger="payment_state in ('reversed','not_paid')" widget="badge" invisible="state in ['draft', 'submit', 'cancel']"/>
|
||||
<field name="state" optional="show"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-success="state in ['approve', 'post', 'done']"
|
||||
decoration-warning="state == 'submit'"
|
||||
decoration-danger="state == 'cancel'"
|
||||
widget="badge"/>
|
||||
<field name="payment_state" optional="show"
|
||||
decoration-info="payment_state in ('partial','in_payment')"
|
||||
decoration-success="payment_state == 'paid'"
|
||||
decoration-danger="payment_state in ('reversed','not_paid')"
|
||||
widget="badge" invisible="state in ['draft', 'submit', 'cancel']"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -686,11 +741,11 @@
|
||||
<form string="Expense Reports">
|
||||
<field name="can_reset" invisible="1"/>
|
||||
<field name="can_approve" invisible="1"/>
|
||||
<field name="company_id" invisible="1" readonly="state != 'draft'"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="account_move_ids" invisible="1"/>
|
||||
<field name="payment_state" invisible="1"/>
|
||||
<field name="is_editable" invisible="1"/>
|
||||
<field name="currency_id" invisible="1" readonly="state != 'draft'"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<header>
|
||||
<button name="action_submit_sheet"
|
||||
@@ -761,8 +816,8 @@
|
||||
class="oe_stat_button"
|
||||
icon="fa-file-text-o"
|
||||
type="object"
|
||||
invisible="expense_number == 0">
|
||||
<field name="expense_number" widget="statinfo" string="Expenses"/>
|
||||
invisible="nb_expense == 0">
|
||||
<field name="nb_expense" widget="statinfo" string="Expenses"/>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Paid" bg_color="text-bg-success" invisible="payment_state != 'paid'"/>
|
||||
@@ -776,23 +831,31 @@
|
||||
</div>
|
||||
<group>
|
||||
<group name="employee_details">
|
||||
<field name="employee_id" context="{'default_company_id': company_id}" widget="many2one_avatar_user" readonly="state != 'draft'"/>
|
||||
<field name="employee_id" context="{'default_company_id': company_id}"
|
||||
widget="many2one_avatar_user" readonly="state != 'draft'"/>
|
||||
<field name="payment_mode"/>
|
||||
<field name="employee_journal_id" groups="account.group_account_invoice,account.group_account_readonly" options="{'no_open': True, 'no_create': True}" invisible="payment_mode != 'own_account'" readonly="state in ['done', 'post']" context="{'default_company_id': company_id}"/>
|
||||
<field name="employee_journal_id"
|
||||
groups="account.group_account_invoice,account.group_account_readonly"
|
||||
options="{'no_open': True, 'no_create': True}"
|
||||
invisible="payment_mode != 'own_account'" readonly="not is_editable"
|
||||
context="{'default_company_id': company_id}"/>
|
||||
<field name="selectable_payment_method_line_ids" invisible="1"/>
|
||||
<field name="payment_method_line_id"
|
||||
context="{'show_payment_journal_id': 1}"
|
||||
options="{'no_open': True, 'no_create': True}"
|
||||
invisible="payment_mode != 'company_account'"
|
||||
readonly="state in ('done', 'post')"
|
||||
readonly="not is_editable"
|
||||
required="payment_mode == 'company_account'"/>
|
||||
<field name="address_id" invisible="1" context="{'default_company_id': company_id}"/>
|
||||
<field name="department_id" invisible="1" readonly="state in ['done', 'post']" context="{'default_company_id': company_id}"/>
|
||||
<field name="department_id" invisible="1" readonly="not is_editable"
|
||||
context="{'default_company_id': company_id}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id" groups="base.group_multi_company" readonly="state != 'draft'"/>
|
||||
<field name="user_id" widget="many2one_avatar_user" readonly="state != 'draft'"/>
|
||||
<field name="accounting_date" groups="account.group_account_invoice,account.group_account_readonly" invisible="state not in ['approve', 'post', 'done']" readonly="state in ['post', 'done']"/>
|
||||
<field name="accounting_date"
|
||||
groups="account.group_account_invoice,account.group_account_readonly"
|
||||
invisible="state not in ['approve', 'post', 'done']"
|
||||
readonly="not is_editable"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
@@ -817,39 +880,45 @@
|
||||
readonly="not is_editable"
|
||||
force_save="1">
|
||||
<tree editable="bottom">
|
||||
<field name="employee_id" column_invisible="True" readonly="state in ['approved', 'done']"/>
|
||||
<field name="employee_id" column_invisible="True"/>
|
||||
<field name="state" column_invisible="True"/>
|
||||
<field name="attachment_number" column_invisible="True"/>
|
||||
<field name="company_id" column_invisible="True" readonly="state not in ['draft', 'refused']"/>
|
||||
<field name="nb_attachment" column_invisible="True"/>
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
<field name="company_currency_id" column_invisible="True"/>
|
||||
<field name="is_multiple_currency" column_invisible="True"/>
|
||||
<field name="product_has_cost" column_invisible="True"/>
|
||||
<field name="date" optional="show" readonly="state == 'done'"/>
|
||||
<field name="product_id" readonly="state == 'done'"/>
|
||||
<field name="name" readonly="state == 'done'"/>
|
||||
<field name="description" optional="hide" readonly="state not in ['draft', 'refused', 'reported']"/>
|
||||
<button name="action_get_attachment_view" type="object" icon="fa-paperclip" aria-label="View Attachments" title="View Attachments" class="float-end pe-0" readonly="1" invisible="attachment_number == 0"/>
|
||||
<field name="reference" optional="hide" readonly="1"/>
|
||||
<field name="date" optional="show"/>
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<field name="description" optional="hide"/>
|
||||
<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" invisible="nb_attachment == 0"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
optional="show"
|
||||
options="{'product_field': 'product_id', 'account_field': 'account_id', 'business_domain': 'expense'}"/>
|
||||
<field name="account_id" optional="hide" groups="account.group_account_readonly"/>
|
||||
<field name="unit_amount" optional="hide" widget="monetary" readonly="not product_has_cost" options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="currency_id" optional="hide" readonly="1" groups="base.group_multi_currency"/>
|
||||
<field name="price_unit" optional="hide" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="currency_id" optional="hide" readonly="True" groups="base.group_multi_currency"/>
|
||||
<field name="quantity" optional="hide" readonly="not product_has_cost"/>
|
||||
<field name="tax_ids" string="Taxes" optional="show" widget="many2many_tags" context="{'default_company_id': company_id}"/>
|
||||
<field name="amount_tax" optional="hide" options="{'currency_field': 'currency_id'}" context="{'default_company_id': company_id}" readonly="1" groups="base.group_multi_currency"/>
|
||||
<field name="amount_tax_company" optional="hide" readonly="1"/>
|
||||
<field name="total_amount" options="{'currency_field': 'currency_id'}" string="Subtotal In Currency " optional="show" readonly="product_has_cost" groups="base.group_multi_currency"/>
|
||||
<field name="total_amount_company" string="Subtotal"/>
|
||||
<field name="tax_ids" string="Taxes" optional="show" widget="many2many_tags"
|
||||
context="{'default_company_id': company_id}"/>
|
||||
<field name="tax_amount_currency" optional="hide" options="{'currency_field': 'currency_id'}"
|
||||
context="{'default_company_id': company_id}" readonly="True" groups="base.group_multi_currency"/>
|
||||
<field name="tax_amount" optional="hide" readonly="True"/>
|
||||
<field name="total_amount_currency" options="{'currency_field': 'currency_id'}"
|
||||
string="Subtotal In Currency " optional="show"
|
||||
readonly="not is_multiple_currency" groups="base.group_multi_currency"/>
|
||||
<field name="total_amount" string="Subtotal" readonly="product_has_cost"/>
|
||||
</tree>
|
||||
</field>
|
||||
<group class="oe_subtotal_footer" colspan="2" name="expense_total">
|
||||
<field name="untaxed_amount"/>
|
||||
<div class="oe_inline o_td_label">
|
||||
<label for="total_amount_taxes"/>
|
||||
<label for="total_tax_amount"/>
|
||||
</div>
|
||||
<field name="total_amount_taxes" nolabel="1"/>
|
||||
<field name="total_tax_amount" nolabel="1"/>
|
||||
<div class="oe_inline o_td_label">
|
||||
<label for="total_amount"/>
|
||||
</div>
|
||||
@@ -888,7 +957,9 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<strong class="o_kanban_record_title"><span><t t-out="record.name.value"/></span></strong>
|
||||
<strong class="o_kanban_record_subtitle float-end"><span class="text-end"><field name="total_amount" widget="monetary"/></span></strong>
|
||||
<strong class="o_kanban_record_subtitle float-end">
|
||||
<span class="text-end"><field name="total_amount" widget="monetary"/></span>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
@@ -897,8 +968,10 @@
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span class="float-end text-end">
|
||||
<field name="state" widget="label_selection" options="{'classes': {'draft': 'default', 'submit': 'default', 'cancel': 'danger', 'post': 'warning',
|
||||
'done': 'success'}}"/>
|
||||
<field name="state" widget="label_selection"
|
||||
options="{'classes': {'draft': 'default', 'submit': 'default',
|
||||
'cancel': 'danger', 'post': 'warning',
|
||||
'done': 'success'}}"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -972,13 +1045,17 @@
|
||||
<field string="Department" name="department_id" operator="child_of"/>
|
||||
<field string="Journal" name="journal_id"/>
|
||||
<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"/>
|
||||
<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 invisible="1"/>
|
||||
<filter string="Not Refused" name="not_refused_reports" domain="[('employee_id.user_id', '=', uid), ('state', '!=', 'cancel')]" invisible="1"/>
|
||||
<filter string="Not Refused" name="not_refused_reports"
|
||||
domain="[('employee_id.user_id', '=', uid), ('state', '!=', 'cancel')]" invisible="1"/>
|
||||
<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"/>
|
||||
<filter domain="[('employee_id.active', '=', False)]" string="Former Employees"
|
||||
name="inactive" groups="hr_expense.group_hr_expense_user,hr_expense.group_hr_expense_manager"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
@@ -990,8 +1067,10 @@
|
||||
<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"/>
|
||||
<filter string="Date" name="expenses_month" domain="[]" context="{'group_by': 'accounting_date'}" help="Expenses by Date"/>
|
||||
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}"
|
||||
groups="base.group_multi_company"/>
|
||||
<filter string="Date" name="expenses_month" domain="[]" context="{'group_by': 'accounting_date'}"
|
||||
help="Expenses by Date"/>
|
||||
<filter string="Status" domain="[]" context="{'group_by': 'state'}" name="state"/>
|
||||
</group>
|
||||
</search>
|
||||
@@ -1023,7 +1102,9 @@
|
||||
<field name="currency_id"/>
|
||||
<templates>
|
||||
<div t-name="activity-box">
|
||||
<img class="rounded" t-att-src="activity_image('hr.employee', 'avatar_128', record.employee_id.raw_value)" t-att-title="record.employee_id.value" t-att-alt="record.employee_id.value"/>
|
||||
<img class="rounded"
|
||||
t-att-src="activity_image('hr.employee', 'avatar_128', record.employee_id.raw_value)"
|
||||
t-att-title="record.employee_id.value" t-att-alt="record.employee_id.value"/>
|
||||
<div class="ms-2">
|
||||
<field name="name" display="full" class="o_text_block"/>
|
||||
<field name="total_amount" widget="monetary" muted="1" display="full"/>
|
||||
@@ -1146,20 +1227,26 @@
|
||||
<menuitem id="menu_hr_expense_root" name="Expenses" sequence="230" web_icon="hr_expense,static/description/icon.png"/>
|
||||
|
||||
<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_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_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"
|
||||
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_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 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"/>
|
||||
<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"/>
|
||||
</odoo>
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="Expenses" string="Expenses" name="hr_expense" groups="hr_expense.group_hr_expense_manager">
|
||||
<block title="Expenses" name="expenses_setting_container">
|
||||
<setting id="create_expense_setting" string="Incoming Emails" help="Create expenses from incoming emails" title="Send an email to this email alias with the receipt in attachment to create an expense in one click. If the first word of the mail subject contains the category's internal reference or the category name, the corresponding category will automatically be set. Type the expense amount in the mail subject to set it on the expense too.">
|
||||
<setting id="create_expense_setting" string="Incoming Emails"
|
||||
help="Create expenses from incoming emails"
|
||||
title="Send an email to this email alias with the receipt in attachment to create an expense in one click. If the first word of the mail subject contains the category's internal reference or the category name, the corresponding category will automatically be set. Type the expense amount in the mail subject to set it on the expense too.">
|
||||
<field name="hr_expense_use_mailgateway"/>
|
||||
<div class="content-group" invisible="not hr_expense_use_mailgateway or alias_domain in ['localhost', '', False]">
|
||||
<div class="mt16">
|
||||
@@ -22,26 +24,35 @@
|
||||
</div>
|
||||
<div class="content-group" invisible="not hr_expense_use_mailgateway or alias_domain not in ['localhost', '', False]">
|
||||
<div class="mt16">
|
||||
<button type="action" name="base_setup.action_general_configuration" icon="oi-arrow-right" string="Setup your domain alias" class="btn-link"/>
|
||||
<button type="action" name="base_setup.action_general_configuration"
|
||||
icon="oi-arrow-right" string="Setup your domain alias" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Reimburse in Payslip" help="Reimburse expenses in payslips" id="hr_payroll_accountant">
|
||||
<field name="module_hr_payroll_expense" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
<setting id="expense_extract_settings" string="Expense Digitalization (OCR)" company_dependent="1" help="Digitalize your receipts with OCR and Artificial Intelligence" title="use OCR to fill data from a picture of the bill">
|
||||
<setting id="expense_extract_settings" string="Expense Digitalization (OCR)" company_dependent="1"
|
||||
help="Digitalize your receipts with OCR and Artificial Intelligence"
|
||||
title="use OCR to fill data from a picture of the bill">
|
||||
<field name="module_hr_expense_extract" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
<setting company_dependent="1" string="Default Category" help="Default expense categories for uploaded expenses" title="Enable users to choose default category for automatically generated expenses.">
|
||||
<setting company_dependent="1" string="Default Category"
|
||||
help="Default expense categories for uploaded expenses"
|
||||
title="Enable users to choose default category for automatically generated expenses.">
|
||||
<field name="expense_product_id"/>
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Accounting">
|
||||
<setting company_dependent="1" help="Default accounting journal for expenses paid by employees." string="Employee Expense Journal">
|
||||
<setting company_dependent="1" help="Default accounting journal for expenses paid by employees."
|
||||
string="Employee Expense Journal">
|
||||
<field name="expense_journal_id"/>
|
||||
</setting>
|
||||
<setting company_dependent="1" string="Payment methods" help="Payment method allowed for expenses paid by company.">
|
||||
<field name="company_expense_allowed_payment_method_line_ids" widget="many2many_tags" placeholder="All payment methods allowed" options="{'no_create': True}" context="{'show_payment_journal_id': 1}"/>
|
||||
<setting company_dependent="1" string="Payment methods"
|
||||
help="Payment method allowed for expenses paid by company.">
|
||||
<field name="company_expense_allowed_payment_method_line_ids" widget="many2many_tags"
|
||||
placeholder="All payment methods allowed" options="{'no_create': True}"
|
||||
context="{'show_payment_journal_id': 1}"/>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<field name="date" readonly="1" />
|
||||
<field name="employee_id" readonly="1" widget="many2one_avatar_user"/>
|
||||
<field name="product_id" readonly="1" />
|
||||
<field name="total_amount_company" readonly="1" />
|
||||
<field name="total_amount" readonly="1" />
|
||||
<field name="name" readonly="1" />
|
||||
<field name="approved_by" readonly="1" widget="many2one_avatar_user"/>
|
||||
<field name="approved_on" readonly="1" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from copy import deepcopy
|
||||
|
||||
from odoo import fields, models, api, Command
|
||||
from odoo.tools import float_compare
|
||||
@@ -12,48 +12,63 @@ class HrExpenseSplit(models.TransientModel):
|
||||
_check_company_auto = True
|
||||
|
||||
def default_get(self, fields):
|
||||
result = super(HrExpenseSplit, self).default_get(fields)
|
||||
result = super().default_get(fields)
|
||||
if 'expense_id' in result:
|
||||
expense = self.env['hr.expense'].browse(result['expense_id'])
|
||||
result['total_amount'] = 0.0
|
||||
result['total_amount_currency'] = 0.0
|
||||
result['name'] = expense.name
|
||||
result['tax_ids'] = expense.tax_ids
|
||||
result['product_id'] = expense.product_id
|
||||
result['company_id'] = expense.company_id
|
||||
result['analytic_distribution'] = expense.analytic_distribution
|
||||
result['analytic_distribution'] = deepcopy(expense.analytic_distribution) or {}
|
||||
result['employee_id'] = expense.employee_id
|
||||
result['currency_id'] = expense.currency_id
|
||||
return result
|
||||
|
||||
name = fields.Char('Description', required=True)
|
||||
wizard_id = fields.Many2one('hr.expense.split.wizard')
|
||||
expense_id = fields.Many2one('hr.expense', string='Expense')
|
||||
product_id = fields.Many2one('product.product', string='Product', required=True, check_company=True)
|
||||
name = fields.Char(string='Description', required=True)
|
||||
wizard_id = fields.Many2one(comodel_name='hr.expense.split.wizard')
|
||||
expense_id = fields.Many2one(comodel_name='hr.expense', string='Expense')
|
||||
product_id = fields.Many2one(comodel_name='product.product', string='Product', required=True, check_company=True)
|
||||
tax_ids = fields.Many2many(
|
||||
'account.tax',
|
||||
comodel_name='account.tax',
|
||||
check_company=True,
|
||||
domain="[('type_tax_use', '=', 'purchase')]",
|
||||
)
|
||||
total_amount = fields.Monetary("Total In Currency", required=True, compute='_compute_from_product_id', store=True, readonly=False)
|
||||
amount_tax = fields.Monetary(string='Tax amount in Currency', compute='_compute_amount_tax')
|
||||
employee_id = fields.Many2one('hr.employee', string="Employee", required=True)
|
||||
company_id = fields.Many2one('res.company')
|
||||
currency_id = fields.Many2one('res.currency')
|
||||
product_has_tax = fields.Boolean("Whether tax is defined on a selected product", compute='_compute_product_has_tax')
|
||||
product_has_cost = fields.Boolean("Is product with non zero cost selected", compute='_compute_from_product_id', store=True)
|
||||
total_amount_currency = fields.Monetary(
|
||||
string="Total In Currency",
|
||||
required=True,
|
||||
compute='_compute_from_product_id', store=True, readonly=False,
|
||||
)
|
||||
tax_amount_currency = fields.Monetary(string='Tax amount in Currency', compute='_compute_tax_amount_currency')
|
||||
employee_id = fields.Many2one(comodel_name='hr.employee', string="Employee", required=True)
|
||||
company_id = fields.Many2one(comodel_name='res.company')
|
||||
currency_id = fields.Many2one(comodel_name='res.currency')
|
||||
product_has_tax = fields.Boolean(
|
||||
string="Whether tax is defined on a selected product",
|
||||
compute='_compute_product_has_tax',
|
||||
)
|
||||
product_has_cost = fields.Boolean(
|
||||
string="Is product with non zero cost selected",
|
||||
compute='_compute_from_product_id', store=True,
|
||||
)
|
||||
|
||||
@api.depends('total_amount', 'tax_ids')
|
||||
def _compute_amount_tax(self):
|
||||
@api.depends('total_amount_currency', 'tax_ids')
|
||||
def _compute_tax_amount_currency(self):
|
||||
for split in self:
|
||||
taxes = split.tax_ids.with_context(force_price_include=True).compute_all(price_unit=split.total_amount, currency=split.currency_id, quantity=1, product=split.product_id)
|
||||
split.amount_tax = taxes['total_included'] - taxes['total_excluded']
|
||||
taxes = split.tax_ids.with_context(force_price_include=True).compute_all(
|
||||
price_unit=split.total_amount_currency,
|
||||
currency=split.currency_id,
|
||||
quantity=1,
|
||||
product=split.product_id
|
||||
)
|
||||
split.tax_amount_currency = taxes['total_included'] - taxes['total_excluded']
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_from_product_id(self):
|
||||
for split in self:
|
||||
split.product_has_cost = split.product_id and (float_compare(split.product_id.standard_price, 0.0, precision_digits=2) != 0)
|
||||
if split.product_has_cost:
|
||||
split.total_amount = split.product_id._price_compute('standard_price', currency=split.currency_id)[split.product_id.id]
|
||||
split.total_amount_currency = split.product_id._price_compute('standard_price', currency=split.currency_id)[split.product_id.id]
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
@@ -61,7 +76,10 @@ class HrExpenseSplit(models.TransientModel):
|
||||
In case we switch to the product without taxes defined on it, taxes should be removed.
|
||||
Computed method won't be good for this purpose, as we don't want to recompute and reset taxes in case they are removed on purpose during splitting.
|
||||
"""
|
||||
self.tax_ids = self.tax_ids if self.product_has_tax and self.tax_ids else self.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.company_id))
|
||||
if self.product_has_tax and self.tax_ids:
|
||||
self.tax_ids = self.tax_ids
|
||||
else:
|
||||
self.tax_ids = self.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.company_id))
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_product_has_tax(self):
|
||||
@@ -73,8 +91,8 @@ class HrExpenseSplit(models.TransientModel):
|
||||
vals = {
|
||||
'name': self.name,
|
||||
'product_id': self.product_id.id,
|
||||
'total_amount': self.total_amount,
|
||||
'total_amount_company': self.expense_id.currency_id.round(self.expense_id.currency_rate * self.total_amount),
|
||||
'total_amount_currency': self.total_amount_currency,
|
||||
'total_amount': self.expense_id.currency_id.round(self.expense_id.currency_rate * self.total_amount_currency),
|
||||
'tax_ids': [Command.set(self.tax_ids.ids)],
|
||||
'analytic_distribution': self.analytic_distribution,
|
||||
'employee_id': self.employee_id.id,
|
||||
|
||||
@@ -1,35 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class HrExpenseSplitWizard(models.TransientModel):
|
||||
_name = 'hr.expense.split.wizard'
|
||||
_description = 'Expense Split Wizard'
|
||||
|
||||
expense_id = fields.Many2one('hr.expense', string='Expense', required=True)
|
||||
expense_split_line_ids = fields.One2many('hr.expense.split', 'wizard_id')
|
||||
total_amount = fields.Monetary('Total Amount', compute='_compute_total_amount', currency_field='currency_id')
|
||||
total_amount_original = fields.Monetary('Total amount original', related='expense_id.total_amount', currency_field='currency_id', help='Total amount of the original Expense that we are splitting')
|
||||
total_amount_taxes = fields.Monetary('Taxes', currency_field='currency_id', compute='_compute_total_amount_taxes')
|
||||
expense_id = fields.Many2one(comodel_name='hr.expense', string='Expense', required=True)
|
||||
expense_split_line_ids = fields.One2many(comodel_name='hr.expense.split', inverse_name='wizard_id')
|
||||
total_amount_currency = fields.Monetary(string='Total Amount', compute='_compute_total_amount_currency', currency_field='currency_id')
|
||||
total_amount_currency_original = fields.Monetary(
|
||||
string='Total amount original', related='expense_id.total_amount_currency',
|
||||
currency_field='currency_id',
|
||||
help='Total amount of the original Expense that we are splitting',
|
||||
)
|
||||
tax_amount_currency = fields.Monetary(
|
||||
string='Taxes',
|
||||
currency_field='currency_id',
|
||||
compute='_compute_tax_amount_currency',
|
||||
)
|
||||
split_possible = fields.Boolean(help='The sum of after split shut remain the same', compute='_compute_split_possible')
|
||||
currency_id = fields.Many2one('res.currency', related='expense_id.currency_id')
|
||||
currency_id = fields.Many2one(comodel_name='res.currency', related='expense_id.currency_id')
|
||||
|
||||
@api.depends('expense_split_line_ids.total_amount')
|
||||
def _compute_total_amount(self):
|
||||
@api.depends('expense_split_line_ids.total_amount_currency')
|
||||
def _compute_total_amount_currency(self):
|
||||
for wizard in self:
|
||||
wizard.total_amount = sum(wizard.expense_split_line_ids.mapped('total_amount'))
|
||||
wizard.total_amount_currency = sum(wizard.expense_split_line_ids.mapped('total_amount_currency'))
|
||||
|
||||
@api.depends('expense_split_line_ids.amount_tax')
|
||||
def _compute_total_amount_taxes(self):
|
||||
@api.depends('expense_split_line_ids.tax_amount_currency')
|
||||
def _compute_tax_amount_currency(self):
|
||||
for wizard in self:
|
||||
wizard.total_amount_taxes = sum(wizard.expense_split_line_ids.mapped('amount_tax'))
|
||||
wizard.tax_amount_currency = sum(wizard.expense_split_line_ids.mapped('tax_amount_currency'))
|
||||
|
||||
@api.depends('total_amount_original', 'total_amount')
|
||||
@api.depends('total_amount_currency_original', 'total_amount_currency')
|
||||
def _compute_split_possible(self):
|
||||
for wizard in self:
|
||||
wizard.split_possible = wizard.total_amount_original and (float_compare(wizard.total_amount_original, wizard.total_amount, precision_digits=2) == 0)
|
||||
wizard.split_possible = wizard.total_amount_currency_original \
|
||||
and wizard.currency_id.compare_amounts(wizard.total_amount_currency_original, wizard.total_amount_currency) == 0
|
||||
|
||||
def action_split_expense(self):
|
||||
self.ensure_one()
|
||||
@@ -48,9 +57,9 @@ class HrExpenseSplitWizard(models.TransientModel):
|
||||
('res_id', '=', self.expense_id.id)
|
||||
])
|
||||
|
||||
for coplied_expense in copied_expenses:
|
||||
for copied_expense in copied_expenses:
|
||||
for attachment in attachment_ids:
|
||||
attachment.copy({'res_model': 'hr.expense', 'res_id': coplied_expense.id})
|
||||
attachment.copy({'res_model': 'hr.expense', 'res_id': copied_expense.id})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="model">hr.expense.split.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="total_amount_original" invisible="1"/>
|
||||
<field name="total_amount_currency_original" invisible="1"/>
|
||||
<field name="expense_id" invisible="1"/>
|
||||
<field name="expense_split_line_ids" widget="one2many" context="{'default_expense_id': expense_id}">
|
||||
<tree editable="bottom">
|
||||
@@ -17,9 +17,9 @@
|
||||
<field name="product_has_cost" column_invisible="True"/>
|
||||
<field name="name"/>
|
||||
<field name="product_id"/>
|
||||
<field name="total_amount" force_save="1" readonly="product_has_cost"/>
|
||||
<field name="total_amount_currency" force_save="1" readonly="product_has_cost"/>
|
||||
<field name="tax_ids" widget="many2many_tags" readonly="not product_has_tax"/>
|
||||
<field name="amount_tax"/>
|
||||
<field name="tax_amount_currency"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
optional="show"
|
||||
groups="analytic.group_analytic_accounting"/>
|
||||
@@ -28,11 +28,11 @@
|
||||
</field>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<group class="oe_subtotal_footer" colspan="2" name="expense_total">
|
||||
<label for="total_amount" invisible="split_possible"/>
|
||||
<field name="total_amount" nolabel="1" class="text-danger" invisible="split_possible"/>
|
||||
<field name="total_amount" invisible="not split_possible"/>
|
||||
<field name="total_amount_original" widget='monetary' string="Original Amount"/>
|
||||
<field name="total_amount_taxes" widget='monetary' string="Taxes"/>
|
||||
<label for="total_amount_currency" invisible="split_possible"/>
|
||||
<field name="total_amount_currency" nolabel="1" class="text-danger" invisible="split_possible"/>
|
||||
<field name="total_amount_currency" invisible="not split_possible"/>
|
||||
<field name="total_amount_currency_original" widget='monetary' string="Original Amount"/>
|
||||
<field name="tax_amount_currency" widget='monetary' string="Taxes"/>
|
||||
</group>
|
||||
<field name="split_possible" invisible="1"/>
|
||||
<footer>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
@@ -90,7 +89,7 @@ class Project(models.Model):
|
||||
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
query = self.env['hr.expense']._search([('state', 'in', ['approved', 'done'])])
|
||||
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query_string, query_param = query.select('currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
|
||||
query_string, query_param = query.select('currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount_currency) as untaxed_amount')
|
||||
query_string = f"{query_string} GROUP BY currency_id"
|
||||
self._cr.execute(query_string, query_param)
|
||||
expenses_read_group = [expense for expense in self._cr.dictfetchall()]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
||||
@@ -46,8 +45,8 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj
|
||||
expense = self.env['hr.expense'].create({
|
||||
'name': 'Car Travel Expenses',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 350.00,
|
||||
'product_id': self.product_c.id,
|
||||
'total_amount_currency': 350.00,
|
||||
'company_id': self.env.company.id,
|
||||
'analytic_distribution': {self.project.analytic_account_id.id: 100},
|
||||
})
|
||||
@@ -65,8 +64,8 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj
|
||||
self.project._get_profitability_items(False),
|
||||
{
|
||||
'costs': {
|
||||
'data': [{'id': 'expenses', 'sequence': expense_sequence, 'to_bill': 0.0, 'billed': -expense.untaxed_amount}],
|
||||
'total': {'to_bill': 0.0, 'billed': -expense.untaxed_amount},
|
||||
'data': [{'id': 'expenses', 'sequence': expense_sequence, 'to_bill': 0.0, 'billed': -expense.untaxed_amount_currency}],
|
||||
'total': {'to_bill': 0.0, 'billed': -expense.untaxed_amount_currency},
|
||||
},
|
||||
'revenues': {'data': [], 'total': {'to_invoice': 0.0, 'invoiced': 0.0}},
|
||||
},
|
||||
@@ -76,8 +75,8 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj
|
||||
expense_foreign = self.env['hr.expense'].create({
|
||||
'name': 'Car Travel Expenses foreign',
|
||||
'employee_id': foreign_employee.id,
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 350.00,
|
||||
'product_id': self.product_c.id,
|
||||
'total_amount_currency': 350.00,
|
||||
'company_id': foreign_company.id,
|
||||
'analytic_distribution': {self.project.analytic_account_id.id: 100},
|
||||
'currency_id': self.foreign_currency.id,
|
||||
@@ -99,9 +98,9 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj
|
||||
'id': 'expenses',
|
||||
'sequence': expense_sequence,
|
||||
'to_bill': 0.0,
|
||||
'billed': -expense.untaxed_amount - expense_foreign.untaxed_amount * 0.2
|
||||
'billed': -expense.untaxed_amount_currency - expense_foreign.untaxed_amount_currency * 0.2
|
||||
}],
|
||||
'total': {'to_bill': 0.0, 'billed': -expense.untaxed_amount - expense_foreign.untaxed_amount * 0.2},
|
||||
'total': {'to_bill': 0.0, 'billed': -expense.untaxed_amount_currency - expense_foreign.untaxed_amount_currency * 0.2},
|
||||
},
|
||||
'revenues': {'data': [], 'total': {'to_invoice': 0.0, 'invoiced': 0.0}},
|
||||
},
|
||||
@@ -113,8 +112,8 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj
|
||||
self.project._get_profitability_items(False),
|
||||
{
|
||||
'costs': {
|
||||
'data': [{'id': 'expenses', 'sequence': expense_sequence, 'to_bill': 0.0, 'billed': -expense_foreign.untaxed_amount * 0.2}],
|
||||
'total': {'to_bill': 0.0, 'billed': -expense_foreign.untaxed_amount * 0.2},
|
||||
'data': [{'id': 'expenses', 'sequence': expense_sequence, 'to_bill': 0.0, 'billed': -expense_foreign.untaxed_amount_currency * 0.2}],
|
||||
'total': {'to_bill': 0.0, 'billed': -expense_foreign.untaxed_amount_currency * 0.2},
|
||||
},
|
||||
'revenues': {'data': [], 'total': {'to_invoice': 0.0, 'invoiced': 0.0}},
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ class Project(models.Model):
|
||||
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
query = self.env['hr.expense']._search([('state', 'in', ['approved', 'done'])])
|
||||
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query_string, query_param = query.select('sale_order_id', 'product_id', 'currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
|
||||
query_string, query_param = query.select('sale_order_id', 'product_id', 'currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount_currency) as untaxed_amount_currency')
|
||||
query_string = f"{query_string} GROUP BY sale_order_id, product_id, currency_id"
|
||||
self._cr.execute(query_string, query_param)
|
||||
expenses_read_group = [expense for expense in self._cr.dictfetchall()]
|
||||
@@ -34,7 +34,7 @@ class Project(models.Model):
|
||||
expenses_per_so_id.setdefault(so_id, {})[product_id] = res['ids']
|
||||
if can_see_expense:
|
||||
expense_ids.extend(res['ids'])
|
||||
dict_amount_per_currency[res['currency_id']] += res['untaxed_amount']
|
||||
dict_amount_per_currency[res['currency_id']] += res['untaxed_amount_currency']
|
||||
for currency_id in dict_amount_per_currency:
|
||||
if currency_id == self.currency_id.id:
|
||||
amount_billed += dict_amount_per_currency[currency_id]
|
||||
|
||||
@@ -39,7 +39,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr
|
||||
expense = self.env['hr.expense'].create({
|
||||
'name': 'expense',
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'total_amount': self.company_data['product_order_sales_price'].list_price,
|
||||
'total_amount_currency': self.company_data['product_order_sales_price'].list_price,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'analytic_distribution': {account.id: 100},
|
||||
'sale_order_id': self.sale_order.id,
|
||||
@@ -64,7 +64,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr
|
||||
'name': 'Expense foreign',
|
||||
'employee_id': foreign_employee.id,
|
||||
'product_id': self.product_c.id, # Foreign currency product must have no cost
|
||||
'total_amount': 350.00 * 0.5, # 0.5 is the exchange rate
|
||||
'total_amount_currency': 350.00 * 0.5, # 0.5 is the exchange rate
|
||||
'company_id': foreign_company.id,
|
||||
'analytic_distribution': {account.id: 100},
|
||||
'currency_id': self.foreign_currency.id,
|
||||
@@ -82,7 +82,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr
|
||||
sequence_per_invoice_type = project._get_profitability_sequence_per_invoice_type()
|
||||
self.assertIn('expenses', sequence_per_invoice_type)
|
||||
expense_sequence = sequence_per_invoice_type['expenses']
|
||||
billed = -expense.untaxed_amount - expense_foreign.untaxed_amount * 0.2 # 280 + 350 * 0.2 = 350
|
||||
billed = -expense.untaxed_amount_currency - expense_foreign.untaxed_amount_currency * 0.2 # 280 + 350 * 0.2 = 350
|
||||
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
@@ -203,7 +203,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr
|
||||
)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('costs', {}),
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0},
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-expense_foreign.untaxed_amount_currency * 0.2), 'to_bill': 0.0},
|
||||
)
|
||||
|
||||
invoice = self.env['sale.advance.payment.inv'].with_context({
|
||||
@@ -236,7 +236,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr
|
||||
)
|
||||
self.assertDictEqual(
|
||||
expense_profitability['costs'],
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-expense_foreign.untaxed_amount * 0.2), 'to_bill': 0.0},
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': expense.currency_id.round(-expense_foreign.untaxed_amount_currency * 0.2), 'to_bill': 0.0},
|
||||
)
|
||||
|
||||
expense_sheet_foreign._do_refuse('Test Cancel Expense')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
@@ -13,8 +12,8 @@ class AccountMoveLine(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.expense_id: # expense flow is different from vendor bill reinvoice flow
|
||||
return self.expense_id.product_id.expense_policy in ['sales_price', 'cost'] and self.expense_id.sale_order_id
|
||||
return super(AccountMoveLine, self)._sale_can_be_reinvoice()
|
||||
return self.expense_id.product_id.expense_policy in {'sales_price', 'cost'} and self.expense_id.sale_order_id
|
||||
return super()._sale_can_be_reinvoice()
|
||||
|
||||
def _sale_determine_order(self):
|
||||
""" For move lines created from expense, we override the normal behavior.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
@@ -20,7 +19,7 @@ class TestReInvoice(TestExpenseCommon, TestSaleCommon):
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'order_line': [(0, 0, {
|
||||
'order_line': [Command.create({
|
||||
'name': self.company_data['product_order_sales_price'].name,
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'product_uom_qty': 2.0,
|
||||
@@ -35,47 +34,47 @@ class TestReInvoice(TestExpenseCommon, TestSaleCommon):
|
||||
'journal_id': self.company_data['default_journal_purchase'].id,
|
||||
'accounting_date': '2017-01-01',
|
||||
'expense_line_ids': [
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_1',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'unit_amount': self.company_data['product_order_sales_price'].list_price,
|
||||
'total_amount_currency': self.company_data['product_order_sales_price'].list_price,
|
||||
'analytic_distribution': {self.analytic_account_1.id: 100},
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_2',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.company_data['product_delivery_sales_price'].id,
|
||||
'unit_amount': self.company_data['product_delivery_sales_price'].list_price,
|
||||
'total_amount_currency': self.company_data['product_delivery_sales_price'].list_price,
|
||||
'analytic_distribution': {self.analytic_account_1.id: 100},
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_3',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'unit_amount': self.company_data['product_order_sales_price'].list_price,
|
||||
'total_amount_currency': self.company_data['product_order_sales_price'].list_price,
|
||||
'analytic_distribution': {self.analytic_account_1.id: 100},
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_4',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.company_data['product_delivery_sales_price'].id,
|
||||
'unit_amount': self.company_data['product_delivery_sales_price'].list_price,
|
||||
'total_amount_currency': self.company_data['product_delivery_sales_price'].list_price,
|
||||
'analytic_distribution': {self.analytic_account_1.id: 100},
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_5',
|
||||
'date': '2016-01-01',
|
||||
'product_id': self.company_data['product_delivery_sales_price'].id,
|
||||
'unit_amount': self.company_data['product_delivery_sales_price'].list_price,
|
||||
'total_amount_currency': self.company_data['product_delivery_sales_price'].list_price,
|
||||
'analytic_distribution': {self.analytic_account_1.id: 100},
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
from odoo.tests import tagged
|
||||
@@ -17,7 +16,7 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'order_line': [(0, 0, {
|
||||
'order_line': [Command.create({
|
||||
'name': self.company_data['product_delivery_no'].name,
|
||||
'product_id': self.company_data['product_delivery_no'].id,
|
||||
'product_uom_qty': 2,
|
||||
@@ -40,7 +39,7 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
'name': 'Air Travel',
|
||||
'product_id': self.company_data['product_delivery_cost'].id,
|
||||
'analytic_distribution': {so.analytic_account_id.id: 100},
|
||||
'unit_amount': 621.54,
|
||||
'quantity': 11.30,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sheet_id': sheet.id,
|
||||
'sale_order_id': so.id,
|
||||
@@ -52,8 +51,8 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
# expense should now be in sales order
|
||||
self.assertIn(self.company_data['product_delivery_cost'], so.mapped('order_line.product_id'), 'Sale Expense: expense product should be in so')
|
||||
sol = so.order_line.filtered(lambda sol: sol.product_id.id == self.company_data['product_delivery_cost'].id)
|
||||
self.assertEqual((sol.price_unit, sol.qty_delivered), (621.54, 1.0), 'Sale Expense: error when invoicing an expense at cost')
|
||||
self.assertEqual(so.amount_total, init_price + sol.price_unit, 'Sale Expense: price of so should be updated after adding expense')
|
||||
self.assertEqual((sol.price_unit, sol.qty_delivered), (55.0, 11.3), 'Sale Expense: error when invoicing an expense at cost')
|
||||
self.assertEqual(so.amount_total, init_price + exp.total_amount, 'Sale Expense: price of so should be updated after adding expense')
|
||||
|
||||
# create some expense and validate it (expense at sale price)
|
||||
init_price = so.amount_total
|
||||
@@ -66,7 +65,7 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
'list_price': 0.50,
|
||||
'uom_id': self.env.ref('uom.product_uom_km').id,
|
||||
'uom_po_id': self.env.ref('uom.product_uom_km').id,
|
||||
'standard_price': 1,
|
||||
'standard_price': 0.15,
|
||||
})
|
||||
# Submit to Manager
|
||||
sheet = self.env['hr.expense.sheet'].create({
|
||||
@@ -79,7 +78,6 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
'product_id': prod_exp_2.id,
|
||||
'analytic_distribution': {so.analytic_account_id.id: 100},
|
||||
'product_uom_id': self.env.ref('uom.product_uom_km').id,
|
||||
'unit_amount': 0.15,
|
||||
'quantity': 100,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sheet_id': sheet.id,
|
||||
@@ -98,4 +96,4 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
|
||||
# both expenses should be invoiced
|
||||
inv = so._create_invoices()
|
||||
self.assertEqual(inv.amount_untaxed, 621.54 + (prod_exp_2.list_price * 100.0), 'Sale Expense: invoicing of expense is wrong')
|
||||
self.assertEqual(inv.amount_untaxed, 621.5 + (prod_exp_2.list_price * 100.0), 'Sale Expense: invoicing of expense is wrong')
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
<xpath expr="//field[@name='analytic_distribution']" position="before">
|
||||
<field name="sale_order_id" groups="!sales_team.group_sale_salesman,!account.group_account_manager"
|
||||
invisible="not can_be_reinvoiced"
|
||||
readonly="not sheet_is_editable"
|
||||
readonly="not is_editable"
|
||||
options="{'no_create_edit': True, 'no_create': True, 'no_open': True}"
|
||||
context="{'sale_show_partner_name': True, 'sale_expense_all_order': True}"
|
||||
widget="sale_order_many2one"/>
|
||||
<field name="sale_order_id" groups="sales_team.group_sale_salesman,!account.group_account_manager"
|
||||
invisible="not can_be_reinvoiced"
|
||||
readonly="not sheet_is_editable"
|
||||
readonly="not is_editable"
|
||||
options="{'no_create_edit': True, 'no_create': True}"
|
||||
context="{'sale_show_partner_name': True, 'sale_expense_all_order': True}"
|
||||
widget="many2one"/>
|
||||
<field name="sale_order_id" groups="account.group_account_manager"
|
||||
widget="many2one"
|
||||
invisible="not can_be_reinvoiced"
|
||||
readonly="state == 'done' or not sheet_is_editable"
|
||||
readonly="not is_editable"
|
||||
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"/>
|
||||
@@ -34,7 +34,7 @@
|
||||
<field name="model">hr.expense</field>
|
||||
<field name="inherit_id" ref="hr_expense.view_expenses_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='reference']" position="after">
|
||||
<xpath expr="//field[@name='date']" position="before">
|
||||
<field name="sale_order_id" optional="hide" invisible="not can_be_reinvoiced" readonly="state in ['done', 'refused']" 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" readonly="1" column_invisible="True"/>
|
||||
</xpath>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, fields
|
||||
@@ -10,16 +9,10 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
@api.depends('is_expense')
|
||||
def _compute_purchase_price(self):
|
||||
date_today = fields.Date.context_today(self)
|
||||
expense_lines = self.filtered('expense_id')
|
||||
for line in expense_lines:
|
||||
if line.expense_id.product_has_cost:
|
||||
product_cost = line.expense_id.untaxed_amount / line.expense_id.quantity
|
||||
else:
|
||||
product_cost = line.expense_id.untaxed_amount
|
||||
|
||||
line.purchase_price = line._convert_to_sol_currency(
|
||||
product_cost,
|
||||
line.expense_id.currency_id)
|
||||
expense = line.expense_id
|
||||
product_cost = expense.untaxed_amount_currency / (expense.quantity or 1.0)
|
||||
line.purchase_price = line._convert_to_sol_currency(product_cost, expense.currency_id)
|
||||
|
||||
return super(SaleOrderLine, self - expense_lines)._compute_purchase_price()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
@@ -11,19 +10,18 @@ class TestExpenseMargin(TestExpenseCommon):
|
||||
# re-invoiceable products
|
||||
product_with_cost = self.product_a
|
||||
product_with_cost.write({'standard_price': 1000, 'expense_policy': 'sales_price'})
|
||||
product_with_no_cost = self.product_b
|
||||
product_with_no_cost.write({'standard_price': 0, 'expense_policy': 'sales_price'})
|
||||
product_with_no_cost = self.product_c
|
||||
product_with_no_cost.write({'expense_policy': 'sales_price'})
|
||||
|
||||
# create SO line and confirm SO (with only one line)
|
||||
sale_order = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'order_line': [(0, 0, {
|
||||
'order_line': [Command.create({
|
||||
'name': product_with_cost.name,
|
||||
'product_id': product_with_cost.id,
|
||||
'product_uom_qty': 2.0,
|
||||
'price_unit': 13.0,
|
||||
})],
|
||||
})
|
||||
|
||||
@@ -36,45 +34,41 @@ class TestExpenseMargin(TestExpenseCommon):
|
||||
'accounting_date': '2020-10-12',
|
||||
'expense_line_ids': [
|
||||
# expense with zero cost product, with 15% tax
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_1',
|
||||
'date': '2020-10-07',
|
||||
'product_id': product_with_no_cost.id,
|
||||
'unit_amount': product_with_no_cost.standard_price,
|
||||
'total_amount': 100,
|
||||
'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)],
|
||||
'total_amount_currency': 100,
|
||||
'tax_ids': [Command.set(self.company_data['default_tax_purchase'].ids)],
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id,
|
||||
}),
|
||||
# expense with zero cost product, with no tax
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_2',
|
||||
'date': '2020-10-07',
|
||||
'product_id': product_with_no_cost.id,
|
||||
'unit_amount': product_with_no_cost.standard_price,
|
||||
'total_amount': 100,
|
||||
'total_amount_currency': 100,
|
||||
'tax_ids': False,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id
|
||||
}),
|
||||
# expense with product with cost (1000), with 15% tax
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_3',
|
||||
'date': '2020-10-07',
|
||||
'product_id': product_with_cost.id,
|
||||
'quantity': 3,
|
||||
'unit_amount': product_with_cost.standard_price,
|
||||
'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)],
|
||||
'tax_ids': [Command.set(self.company_data['default_tax_purchase'].ids)],
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id
|
||||
}),
|
||||
# expense with product with cost (1000), with no tax
|
||||
(0, 0, {
|
||||
Command.create({
|
||||
'name': 'expense_4',
|
||||
'date': '2020-10-07',
|
||||
'product_id': product_with_cost.id,
|
||||
'quantity': 5,
|
||||
'unit_amount': product_with_cost.standard_price,
|
||||
'tax_ids': False,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'sale_order_id': sale_order.id
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"cells": {
|
||||
"A7": {
|
||||
"style": 1,
|
||||
"content": "[Expenses Analysis](odoo://view/{\"viewType\":\"graph\",\"action\":{\"domain\":[],\"context\":{\"group_by\":[\"date:month\",\"product_id\"],\"graph_measure\":\"total_amount_company\",\"graph_mode\":\"bar\",\"graph_groupbys\":[\"date:month\",\"product_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\",\"positional\":true})",
|
||||
"content": "[Expenses Analysis](odoo://view/{\"viewType\":\"graph\",\"action\":{\"domain\":[],\"context\":{\"group_by\":[\"date:month\",\"product_id\"],\"graph_measure\":\"total_amount\",\"graph_mode\":\"bar\",\"graph_groupbys\":[\"date:month\",\"product_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\",\"positional\":true})",
|
||||
"border": 1
|
||||
},
|
||||
"A19": {
|
||||
@@ -105,7 +105,7 @@
|
||||
},
|
||||
"A32": {
|
||||
"style": 1,
|
||||
"content": "[Top Reinvoiced Orders](odoo://view/{\"viewType\":\"pivot\",\"action\":{\"domain\":[[\"sale_order_id\",\"!=\",false]],\"context\":{\"group_by\":[\"sale_order_id\"],\"pivot_measures\":[\"__count\",\"total_amount_company\"],\"pivot_column_groupby\":[],\"pivot_row_groupby\":[\"sale_order_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\"})",
|
||||
"content": "[Top Reinvoiced Orders](odoo://view/{\"viewType\":\"pivot\",\"action\":{\"domain\":[[\"sale_order_id\",\"!=\",false]],\"context\":{\"group_by\":[\"sale_order_id\"],\"pivot_measures\":[\"__count\",\"total_amount\"],\"pivot_column_groupby\":[],\"pivot_row_groupby\":[\"sale_order_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\"})",
|
||||
"border": 1
|
||||
},
|
||||
"A33": {
|
||||
@@ -250,38 +250,38 @@
|
||||
},
|
||||
"C21": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.LIST(1,1,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,1,\"total_amount\")"
|
||||
},
|
||||
"C22": {
|
||||
"content": "=ODOO.LIST(1,2,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,2,\"total_amount\")"
|
||||
},
|
||||
"C23": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.LIST(1,3,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,3,\"total_amount\")"
|
||||
},
|
||||
"C24": {
|
||||
"content": "=ODOO.LIST(1,4,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,4,\"total_amount\")"
|
||||
},
|
||||
"C25": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.LIST(1,5,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,5,\"total_amount\")"
|
||||
},
|
||||
"C26": {
|
||||
"content": "=ODOO.LIST(1,6,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,6,\"total_amount\")"
|
||||
},
|
||||
"C27": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.LIST(1,7,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,7,\"total_amount\")"
|
||||
},
|
||||
"C28": {
|
||||
"content": "=ODOO.LIST(1,8,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,8,\"total_amount\")"
|
||||
},
|
||||
"C29": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.LIST(1,9,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,9,\"total_amount\")"
|
||||
},
|
||||
"C30": {
|
||||
"content": "=ODOO.LIST(1,10,\"total_amount_company\")"
|
||||
"content": "=ODOO.LIST(1,10,\"total_amount\")"
|
||||
},
|
||||
"C33": {
|
||||
"style": 8,
|
||||
@@ -290,42 +290,42 @@
|
||||
},
|
||||
"C34": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",1)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",1)"
|
||||
},
|
||||
"C35": {
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",2)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",2)"
|
||||
},
|
||||
"C36": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",3)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",3)"
|
||||
},
|
||||
"C37": {
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",4)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",4)"
|
||||
},
|
||||
"C38": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",5)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",5)"
|
||||
},
|
||||
"C39": {
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",6)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",6)"
|
||||
},
|
||||
"C40": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",7)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",7)"
|
||||
},
|
||||
"C41": {
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",8)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",8)"
|
||||
},
|
||||
"C42": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",9)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",9)"
|
||||
},
|
||||
"C43": {
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount_company\",\"#sale_order_id\",10)"
|
||||
"content": "=ODOO.PIVOT(9,\"total_amount\",\"#sale_order_id\",10)"
|
||||
},
|
||||
"E19": {
|
||||
"style": 1,
|
||||
"content": "[Top Categories](odoo://view/{\"viewType\":\"pivot\",\"action\":{\"domain\":[[\"product_id\",\"!=\",false]],\"context\":{\"group_by\":[\"product_id\"],\"pivot_measures\":[\"__count\",\"total_amount_company\"],\"pivot_column_groupby\":[],\"pivot_row_groupby\":[\"product_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\"})",
|
||||
"content": "[Top Categories](odoo://view/{\"viewType\":\"pivot\",\"action\":{\"domain\":[[\"product_id\",\"!=\",false]],\"context\":{\"group_by\":[\"product_id\"],\"pivot_measures\":[\"__count\",\"total_amount\"],\"pivot_column_groupby\":[],\"pivot_row_groupby\":[\"product_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\"})",
|
||||
"border": 1
|
||||
},
|
||||
"E20": {
|
||||
@@ -375,7 +375,7 @@
|
||||
},
|
||||
"E32": {
|
||||
"style": 1,
|
||||
"content": "[Top Employees](odoo://view/{\"viewType\":\"pivot\",\"action\":{\"domain\":[[\"product_id\",\"!=\",false]],\"context\":{\"group_by\":[\"product_id\"],\"pivot_measures\":[\"__count\",\"total_amount_company\"],\"pivot_column_groupby\":[],\"pivot_row_groupby\":[\"product_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\"})",
|
||||
"content": "[Top Employees](odoo://view/{\"viewType\":\"pivot\",\"action\":{\"domain\":[[\"product_id\",\"!=\",false]],\"context\":{\"group_by\":[\"product_id\"],\"pivot_measures\":[\"__count\",\"total_amount\"],\"pivot_column_groupby\":[],\"pivot_row_groupby\":[\"product_id\"]},\"modelName\":\"hr.expense\",\"views\":[[false,\"graph\"],[false,\"pivot\"],[false,\"list\"],[false,\"kanban\"],[false,\"form\"],[false,\"search\"]]},\"threshold\":0,\"name\":\"Expenses Analysis\"})",
|
||||
"border": 1
|
||||
},
|
||||
"E33": {
|
||||
@@ -510,38 +510,38 @@
|
||||
},
|
||||
"G21": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",1)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",1)"
|
||||
},
|
||||
"G22": {
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",2)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",2)"
|
||||
},
|
||||
"G23": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",3)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",3)"
|
||||
},
|
||||
"G24": {
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",4)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",4)"
|
||||
},
|
||||
"G25": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",5)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",5)"
|
||||
},
|
||||
"G26": {
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",6)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",6)"
|
||||
},
|
||||
"G27": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",7)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",7)"
|
||||
},
|
||||
"G28": {
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",8)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",8)"
|
||||
},
|
||||
"G29": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",9)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",9)"
|
||||
},
|
||||
"G30": {
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount_company\",\"#product_id\",10)"
|
||||
"content": "=ODOO.PIVOT(1,\"total_amount\",\"#product_id\",10)"
|
||||
},
|
||||
"G33": {
|
||||
"style": 8,
|
||||
@@ -550,38 +550,38 @@
|
||||
},
|
||||
"G34": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",1)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",1)"
|
||||
},
|
||||
"G35": {
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",2)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",2)"
|
||||
},
|
||||
"G36": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",3)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",3)"
|
||||
},
|
||||
"G37": {
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",4)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",4)"
|
||||
},
|
||||
"G38": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",5)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",5)"
|
||||
},
|
||||
"G39": {
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",6)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",6)"
|
||||
},
|
||||
"G40": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",7)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",7)"
|
||||
},
|
||||
"G41": {
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",8)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",8)"
|
||||
},
|
||||
"G42": {
|
||||
"style": 7,
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",9)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",9)"
|
||||
},
|
||||
"G43": {
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount_company\",\"#employee_id\",10)"
|
||||
"content": "=ODOO.PIVOT(4,\"total_amount\",\"#employee_id\",10)"
|
||||
},
|
||||
"A8": {
|
||||
"border": 2
|
||||
@@ -665,7 +665,7 @@
|
||||
"date:month",
|
||||
"product_id"
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": null,
|
||||
"resModel": "hr.expense"
|
||||
},
|
||||
@@ -811,13 +811,13 @@
|
||||
"content": "=ODOO.PIVOT(5,\"__count\")"
|
||||
},
|
||||
"B2": {
|
||||
"content": "=ODOO.PIVOT(6,\"total_amount_company\")"
|
||||
"content": "=ODOO.PIVOT(6,\"total_amount\")"
|
||||
},
|
||||
"B3": {
|
||||
"content": "=ODOO.PIVOT(7,\"total_amount_company\")"
|
||||
"content": "=ODOO.PIVOT(7,\"total_amount\")"
|
||||
},
|
||||
"B4": {
|
||||
"content": "=ODOO.PIVOT(8,\"total_amount_company\")"
|
||||
"content": "=ODOO.PIVOT(8,\"total_amount\")"
|
||||
},
|
||||
"C1": {
|
||||
"style": 7,
|
||||
@@ -921,8 +921,8 @@
|
||||
"payment_mode",
|
||||
"activity_ids",
|
||||
"company_id",
|
||||
"attachment_number",
|
||||
"total_amount_company",
|
||||
"nb_attachment",
|
||||
"total_amount",
|
||||
"state"
|
||||
],
|
||||
"domain": [],
|
||||
@@ -930,7 +930,7 @@
|
||||
"context": {},
|
||||
"orderBy": [
|
||||
{
|
||||
"name": "total_amount_company",
|
||||
"name": "total_amount",
|
||||
"asc": false
|
||||
}
|
||||
],
|
||||
@@ -956,7 +956,7 @@
|
||||
"field": "__count"
|
||||
},
|
||||
{
|
||||
"field": "total_amount_company"
|
||||
"field": "total_amount"
|
||||
}
|
||||
],
|
||||
"model": "hr.expense",
|
||||
@@ -969,7 +969,7 @@
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": "desc"
|
||||
}
|
||||
},
|
||||
@@ -989,7 +989,7 @@
|
||||
"field": "__count"
|
||||
},
|
||||
{
|
||||
"field": "total_amount_company"
|
||||
"field": "total_amount"
|
||||
}
|
||||
],
|
||||
"model": "hr.expense",
|
||||
@@ -1002,7 +1002,7 @@
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": "desc"
|
||||
}
|
||||
},
|
||||
@@ -1024,7 +1024,7 @@
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": "desc"
|
||||
}
|
||||
},
|
||||
@@ -1041,7 +1041,7 @@
|
||||
"id": "6",
|
||||
"measures": [
|
||||
{
|
||||
"field": "total_amount_company"
|
||||
"field": "total_amount"
|
||||
}
|
||||
],
|
||||
"model": "hr.expense",
|
||||
@@ -1052,7 +1052,7 @@
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": "desc"
|
||||
}
|
||||
},
|
||||
@@ -1069,7 +1069,7 @@
|
||||
"id": "7",
|
||||
"measures": [
|
||||
{
|
||||
"field": "total_amount_company"
|
||||
"field": "total_amount"
|
||||
}
|
||||
],
|
||||
"model": "hr.expense",
|
||||
@@ -1080,7 +1080,7 @@
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": "desc"
|
||||
}
|
||||
},
|
||||
@@ -1097,7 +1097,7 @@
|
||||
"id": "8",
|
||||
"measures": [
|
||||
{
|
||||
"field": "total_amount_company"
|
||||
"field": "total_amount"
|
||||
}
|
||||
],
|
||||
"model": "hr.expense",
|
||||
@@ -1108,7 +1108,7 @@
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": "desc"
|
||||
}
|
||||
},
|
||||
@@ -1136,7 +1136,7 @@
|
||||
"field": "__count"
|
||||
},
|
||||
{
|
||||
"field": "total_amount_company"
|
||||
"field": "total_amount"
|
||||
}
|
||||
],
|
||||
"model": "hr.expense",
|
||||
@@ -1149,7 +1149,7 @@
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"measure": "total_amount_company",
|
||||
"measure": "total_amount",
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user