19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from . import hr_expense_refuse_reason
from . import account_payment_register
from . import hr_expense_refuse_reason
from . import hr_expense_post_wizard
from . import hr_expense_approve_duplicate
from . import hr_expense_split_wizard
from . import hr_expense_split

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo import models, api
class AccountPaymentRegister(models.TransientModel):
@ -14,9 +12,13 @@ class AccountPaymentRegister(models.TransientModel):
def _get_line_batch_key(self, line):
# OVERRIDE to set the bank account defined on the employee
res = super()._get_line_batch_key(line)
expense_sheet = self.env['hr.expense.sheet'].search([('payment_mode', '=', 'own_account'), ('account_move_id', 'in', line.move_id.ids)])
if expense_sheet and not line.move_id.partner_bank_id:
res['partner_bank_id'] = expense_sheet.employee_id.sudo().bank_account_id.id or line.partner_id.bank_ids and line.partner_id.bank_ids.ids[0]
expense = line.move_id.expense_ids.filtered(lambda expense: expense.payment_mode == 'own_account')
if expense and not line.move_id.partner_bank_id:
res['partner_bank_id'] = (
expense.employee_id.sudo().primary_bank_account_id.id
or line.partner_id.bank_ids
and line.partner_id.bank_ids.ids[0]
)
return res
def _init_payments(self, to_process, edit_mode=False):
@ -25,15 +27,5 @@ class AccountPaymentRegister(models.TransientModel):
for payment, vals in zip(payments, to_process):
expenses = vals['batch']['lines'].expense_id
if expenses:
payment.line_ids.write({'expense_id': expenses[0].id})
payment.move_id.line_ids.write({'expense_id': expenses[0].id})
return payments
def _reconcile_payments(self, to_process, edit_mode=False):
# OVERRIDE
res = super()._reconcile_payments(to_process, edit_mode=edit_mode)
for vals in to_process:
expense_sheets = vals['batch']['lines'].expense_id.sheet_id
for expense_sheet in expense_sheets:
if expense_sheet.currency_id.is_zero(expense_sheet.amount_residual):
expense_sheet.state = 'done'
return res

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import Command, api, fields, models, _
class HrExpenseApproveDuplicate(models.TransientModel):
@ -10,25 +9,22 @@ class HrExpenseApproveDuplicate(models.TransientModel):
approved. The user has the opportunity to still validate it or decline.
"""
_name = "hr.expense.approve.duplicate"
_name = 'hr.expense.approve.duplicate'
_description = "Expense Approve Duplicate"
sheet_ids = fields.Many2many('hr.expense.sheet')
expense_ids = fields.Many2many('hr.expense', readonly=True)
@api.model
def default_get(self, fields):
res = super().default_get(fields)
if 'sheet_ids' in fields:
res['sheet_ids'] = [(6, 0, self.env.context.get('default_sheet_ids', []))]
if 'duplicate_expense_ids' in fields:
res['expense_ids'] = [(6, 0, self.env.context.get('default_expense_ids', []))]
res['expense_ids'] = [Command.set(self.env.context.get('default_expense_ids', []))]
return res
def action_approve(self):
self.sheet_ids._do_approve()
self.expense_ids.filtered(lambda expense: expense.state == 'submitted')._do_approve()
return {'type': 'ir.actions.act_window_close'}
def action_refuse(self):
self.sheet_ids.refuse_sheet(_('Duplicate Expense'))
self.expense_ids.filtered(lambda expense: expense.state == 'submitted')._do_refuse(_('Duplicate Expense'))
return {'type': 'ir.actions.act_window_close'}

View file

@ -4,23 +4,22 @@
<field name="model">hr.expense.approve.duplicate</field>
<field name="arch" type="xml">
<form string="Expense Validate Duplicate">
<field name="sheet_ids" invisible="1" />
<p>The following approved expenses have similar employee, amount and category than some expenses of this report. Please verify this report does not contain duplicates.</p>
<field name="expense_ids" nolabel="1">
<tree>
<list>
<field name="date" readonly="1" />
<field name="employee_id" readonly="1" />
<field name="employee_id" readonly="1" widget="many2one_avatar_employee"/>
<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" />
<field name="approved_on" readonly="1" />
</tree>
<field name="manager_id" readonly="1" widget="many2one_avatar_user"/>
<field name="approval_date" readonly="1" />
</list>
</field>
<footer>
<button string="Refuse" class="btn-primary" name="action_refuse" type="object" attrs="{'invisible': [('sheet_ids', '=', [])]}" data-hotkey="q" />
<button string="Approve" class="btn-secondary" name="action_approve" type="object" attrs="{'invisible': [('sheet_ids', '=', [])]}" data-hotkey="w" />
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Refuse" class="btn-primary" name="action_refuse" type="object" invisible="not expense_ids" data-hotkey="q" />
<button string="Approve" class="btn-secondary" name="action_approve" type="object" invisible="not expense_ids" data-hotkey="w" />
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>

View file

@ -0,0 +1,85 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class HrExpensePostWizard(models.TransientModel):
_name = 'hr.expense.post.wizard'
_description = 'Expense Posting Wizard'
@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 company to be the same.
"""
company_journal_id = self.env.company.expense_journal_id
if company_journal_id:
return company_journal_id.id
closest_parent_company_journal = self.env.company.parent_ids[::-1].expense_journal_id[:1]
if closest_parent_company_journal:
return closest_parent_company_journal.id
journal = self.env['account.journal'].search([
*self.env['account.journal']._check_company_domain(self.env.company.id),
('type', '=', 'purchase'),
], limit=1)
return journal.id
company_id = fields.Many2one(comodel_name='res.company', default=lambda self: self.env.company, string='Company', readonly=True)
accounting_date = fields.Date( # The date used for the accounting entries or the one we'd like to use if not yet posted
string="Accounting Date",
default=fields.Date.context_today,
help="Specify the bill date of the related vendor bill."
)
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.",
)
def action_post_entry(self):
expenses = self.env['hr.expense'].browse(self.env.context['active_ids'])
if not self.env['account.move'].has_access('create'):
raise UserError(_("You don't have the rights to create accounting entries."))
expense_receipt_vals_list = [
{
**new_receipt_vals,
'journal_id': self.employee_journal_id.id,
'invoice_date': self.accounting_date,
}
for new_receipt_vals in expenses._prepare_receipts_vals()
]
moves_sudo = self.env['account.move'].sudo().create(expense_receipt_vals_list)
for move_sudo in moves_sudo:
move_sudo._message_set_main_attachment_id(move_sudo.attachment_ids, force=True, filter_xml=False)
moves_sudo.action_post()
if not self.company_id.expense_journal_id: # Sets the default one if not specified
self.sudo().company_id.expense_journal_id = self.employee_journal_id.id
# Add the company_paid ids to the redirect
moves_ids = moves_sudo.ids + self.env.context.get('company_paid_move_ids', tuple())
action = {
'type': 'ir.actions.act_window',
'res_model': 'account.move',
}
if len(moves_ids) == 1:
action.update({
'name': moves_sudo.ref,
'view_mode': 'form',
'res_id': moves_ids[0],
})
else:
list_view = self.env.ref('hr_expense.view_move_list_expense', raise_if_not_found=False)
action.update({
'name': _("New expense entries"),
'view_mode': 'list,form',
'views': [(list_view and list_view.id, 'list'), (False, 'form')],
'domain': [('id', 'in', moves_ids)],
})
return action

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="hr_expense.hr_expense_post_wizard_view" model="ir.ui.view">
<field name="name">Post Expenses</field>
<field name="model">hr.expense.post.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="employee_journal_id" string="Journal"/>
<field name="accounting_date" string="Accounting date"/>
</group>
<footer>
<button name="action_post_entry" string="Post Expenses" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>

View file

@ -1,45 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class HrExpenseRefuseWizard(models.TransientModel):
"""This wizard can be launched from an he.expense (an expense line)
or from an hr.expense.sheet (En expense report)
'hr_expense_refuse_model' must be passed in the context to differentiate
the right model to use.
"""
""" Wizard to specify reason on expense refusal """
_name = "hr.expense.refuse.wizard"
_name = 'hr.expense.refuse.wizard'
_description = "Expense Refuse Reason Wizard"
reason = fields.Char(string='Reason', required=True)
hr_expense_ids = fields.Many2many('hr.expense')
hr_expense_sheet_id = fields.Many2one('hr.expense.sheet')
expense_ids = fields.Many2many(comodel_name='hr.expense')
@api.model
def default_get(self, fields):
res = super(HrExpenseRefuseWizard, self).default_get(fields)
active_ids = self.env.context.get('active_ids', [])
refuse_model = self.env.context.get('hr_expense_refuse_model')
if refuse_model == 'hr.expense':
res.update({
'hr_expense_ids': active_ids,
'hr_expense_sheet_id': False,
})
elif refuse_model == 'hr.expense.sheet':
res.update({
'hr_expense_sheet_id': active_ids[0] if active_ids else False,
'hr_expense_ids': [],
})
res = super().default_get(fields)
if 'expense_ids' in fields:
res['expense_ids'] = self.env.context.get('active_ids', [])
return res
def expense_refuse_reason(self):
self.ensure_one()
if self.hr_expense_ids:
self.hr_expense_ids.refuse_expense(self.reason)
if self.hr_expense_sheet_id:
self.hr_expense_sheet_id.refuse_sheet(self.reason)
def action_refuse(self):
self.expense_ids._do_refuse(self.reason)
return {'type': 'ir.actions.act_window_close'}

View file

@ -5,13 +5,13 @@
<field name="model">hr.expense.refuse.wizard</field>
<field name="arch" type="xml">
<form string="Expense refuse reason">
<separator string="Reason to refuse Expense"/>
<field name="hr_expense_ids" invisible="1"/>
<field name="hr_expense_sheet_id" invisible="1"/>
<field name="reason" class="w-100"/>
<field name="expense_ids" invisible="1"/>
<group>
<field string="Reason" name="reason" class="w-100"/>
</group>
<footer>
<button string='Refuse' name="expense_refuse_reason" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="oe_link" special="cancel" data-hotkey="z"/>
<button string='Refuse' name="action_refuse" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="oe_link" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
@ -23,5 +23,6 @@
<field name="view_mode">form</field>
<field name="view_id" ref="hr_expense_refuse_wizard_view_form"/>
<field name="target">new</field>
<field name="context">{'dialog_size': 'medium'}</field>
</record>
</odoo>

View file

@ -1,55 +1,88 @@
# -*- 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
from odoo import fields, models, api, Command
from odoo.tools import float_compare
from odoo.addons.hr_expense.models.hr_expense import EXPENSE_APPROVAL_STATE
class HrExpenseSplit(models.TransientModel):
_name = 'hr.expense.split'
_inherit = ['analytic.mixin']
_description = 'Expense Split'
_check_company_auto = True
@api.model
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
result['approval_state'] = expense.approval_state
result['approval_date'] = expense.approval_date
result['manager_id'] = expense.manager_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, domain="[('can_be_expensed', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]")
tax_ids = fields.Many2many('account.tax', domain="[('company_id', '=', company_id), ('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)
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, domain=[('can_be_expensed', '=', True)],)
tax_ids = fields.Many2many(
comodel_name='account.tax',
check_company=True,
domain="[('type_tax_use', '=', 'purchase')]",
)
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,
)
approval_state = fields.Selection(selection=EXPENSE_APPROVAL_STATE, copy=False, readonly=True)
approval_date = fields.Datetime(string="Approval Date", readonly=True)
manager_id = fields.Many2one(
comodel_name='res.users',
string="Manager",
readonly=True,
domain=lambda self: [('all_group_ids', 'in', self.env.ref('hr_expense.group_hr_expense_team_approver').id)],
)
@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):
@ -57,27 +90,33 @@ 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(lambda tax: tax.company_id == 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):
for split in self:
split.product_has_tax = split.product_id and split.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == split.company_id)
split.product_has_tax = split.product_id and split.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(split.company_id))
def _get_values(self):
self.ensure_one()
vals = {
'name': self.name,
'product_id': self.product_id.id,
'total_amount': self.total_amount,
'tax_ids': [(6, 0, self.tax_ids.ids)],
'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,
'product_uom_id': self.product_id.uom_id.id,
'unit_amount': self.product_id.price_compute('standard_price', currency=self.currency_id)[self.product_id.id]
'approval_state': self.approval_state,
'approval_date': self.approval_date,
'manager_id': self.manager_id.id,
}
account = self.product_id.product_tmpl_id._get_product_accounts()['expense']
account = self.product_id.with_company(self.company_id).product_tmpl_id._get_product_accounts()['expense']
if account:
vals['account_id'] = account.id
return vals

View file

@ -1,35 +1,43 @@
# -*- 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()
@ -41,22 +49,21 @@ class HrExpenseSplitWizard(models.TransientModel):
self.expense_split_line_ids -= expense_split
if self.expense_split_line_ids:
for split in self.expense_split_line_ids:
copied_expenses |= self.expense_id.copy(split._get_values())
copied_expenses |= self.expense_id.with_context({'from_split_wizard': True}).copy(split._get_values())
attachment_ids = self.env['ir.attachment'].search([
('res_model', '=', 'hr.expense'),
('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',
'res_model': 'hr.expense',
'name': _('Split Expenses'),
'view_mode': 'tree,form',
'target': 'current',
'domain': [('id', 'in', (copied_expenses | self.expense_split_line_ids.expense_id).ids)],
}
split_expense_ids = self.env['hr.expense']
if self.expense_id.split_expense_origin_id:
split_expense_ids = self.env['hr.expense'].search([('split_expense_origin_id', '=', self.expense_id.split_expense_origin_id.id)])
(self.expense_id | copied_expenses).split_expense_origin_id = self.expense_id.split_expense_origin_id or self.expense_id
all_related_expenses = copied_expenses | self.expense_id | split_expense_ids
return all_related_expenses._get_records_action(name=_("Split Expenses"))

View file

@ -6,39 +6,49 @@
<field name="model">hr.expense.split.wizard</field>
<field name="arch" type="xml">
<form>
<field name="total_amount_original" invisible="1"/>
<div class="alert alert-warning w-100 d-flex align-items-center gap-1"
invisible="split_possible" role="alert">
<span>The total amount doesn't match the original amount.</span>
</div>
<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">
<field name="currency_id" invisible="1"/>
<field name="expense_id" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="product_has_tax" invisible="1"/>
<field name="product_has_cost" invisible="1"/>
<list editable="bottom">
<field name="currency_id" column_invisible="True"/>
<field name="expense_id" column_invisible="True"/>
<field name="company_id" column_invisible="True"/>
<field name="product_has_cost" column_invisible="True"/>
<field name="name"/>
<field name="product_id"/>
<field name="total_amount" force_save="1" attrs="{'readonly': [('product_has_cost', '=', True)]}"/>
<field name="tax_ids" widget="many2many_tags" attrs="{'readonly': [('product_has_tax', '=', False)]}"/>
<field name="amount_tax"/>
<field name="product_id"
context="{
'default_type': 'service',
'default_can_be_expensed': 1,
'list_view_ref': 'hr_expense.product_product_expense_tree_view',
'form_view_ref': 'hr_expense.product_product_expense_form_view',
}"
/>
<field name="employee_id" widget="many2one_avatar_user"/>
<field name="tax_ids" widget="many2many_tags"/>
<field name="tax_amount_currency"/>
<field name="analytic_distribution" widget="analytic_distribution"
optional="show"
groups="analytic.group_analytic_accounting"/>
<field name="employee_id" widget="many2one_avatar_employee"/>
</tree>
<field name="total_amount_currency" force_save="1" readonly="product_has_cost"/>
</list>
</field>
<field name="currency_id" invisible="1"/>
<group class="oe_subtotal_footer oe_right" colspan="2" name="expense_total">
<label for="total_amount" attrs="{'invisible': [('split_possible', '=', True)]}"/>
<field name="total_amount" nolabel="1" class="text-danger" attrs="{'invisible': [('split_possible', '=', True)]}"/>
<field name="total_amount" attrs="{'invisible': [('split_possible', '=', False)]}"/>
<field name="total_amount_original" widget='monetary' string="Original Amount"/>
<field name="total_amount_taxes" widget='monetary' string="Taxes"/>
<group class="oe_subtotal_footer" colspan="2" name="expense_total">
<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>
<button name="action_split_expense" attrs="{'invisible': [ ('split_possible', '=', True)]}" string="Split Expense" type="object" class="oe_highlight" disabled="disabled" data-hotkey="q"/>
<button name="action_split_expense" string="Split Expense" attrs="{'invisible': [('split_possible', '=', False)]}" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button name="action_split_expense" invisible="split_possible" string="Split Expense" type="object" class="oe_highlight" disabled="disabled" data-hotkey="q"/>
<button name="action_split_expense" string="Split Expense" invisible="not split_possible" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>