[IMP] hr_expense: do not create purchase.receipt
Rework of the hr.expense workflow so that: - expenses paid by employee generate purchase.order - expenses paid by company generate entry that look like payments Main reason being that purchase.receipt are not active by default. That makes the entry hard to find, holes in sequences, inconsitency with payment states. It also remove the refusal of expenses as this was dead / inaccessible code, only reports can be refused. task-id: 3126550 [community](https://github.com/odoo/odoo/pull/110518) [enterprise](https://github.com/odoo/enterprise/pull/36090) closes odoo/odoo#110518 Related: odoo/upgrade#4266 Related: odoo/enterprise#36090 Signed-off-by: Laurent Smet <las@odoo.com>
This commit is contained in:
@@ -39,6 +39,7 @@ You could use this simplified accounting in case you work with an (external) acc
|
||||
'views/account_tax_views.xml',
|
||||
'views/account_full_reconcile_views.xml',
|
||||
'views/account_payment_term_views.xml',
|
||||
'views/account_payment_method.xml',
|
||||
'views/res_partner_bank_views.xml',
|
||||
'views/report_statement.xml',
|
||||
'views/terms_template.xml',
|
||||
|
||||
@@ -2092,7 +2092,7 @@ class AccountMove(models.Model):
|
||||
# Only invoice-like and journal entries in "auto tax mode" are synced
|
||||
tax_container['records'] = container['records'].filtered(lambda m: (m.is_invoice(True) or m.line_ids.tax_ids and not m.tax_cash_basis_origin_move_id))
|
||||
invoice_container['records'] = container['records'].filtered(lambda m: m.is_invoice(True))
|
||||
misc_container['records'] = container['records'].filtered(lambda m: m.move_type == 'entry' and not m.tax_cash_basis_origin_move_id)
|
||||
misc_container['records'] = container['records'].filtered(lambda m: m.is_entry() and not m.tax_cash_basis_origin_move_id)
|
||||
|
||||
tax_container, invoice_container, misc_container = ({} for __ in range(3))
|
||||
update_containers()
|
||||
@@ -3672,6 +3672,9 @@ class AccountMove(models.Model):
|
||||
def is_invoice(self, include_receipts=False):
|
||||
return self.is_sale_document(include_receipts) or self.is_purchase_document(include_receipts)
|
||||
|
||||
def is_entry(self):
|
||||
return self.move_type == 'entry'
|
||||
|
||||
@api.model
|
||||
def get_sale_types(self, include_receipts=False):
|
||||
return ['out_invoice', 'out_refund'] + (include_receipts and ['out_receipt'] or [])
|
||||
|
||||
@@ -119,6 +119,11 @@ class AccountPaymentMethodLine(models.Model):
|
||||
company_id = fields.Many2one(related='journal_id.company_id')
|
||||
available_payment_method_ids = fields.Many2many(related='journal_id.available_payment_method_ids')
|
||||
|
||||
def name_get(self):
|
||||
if self.env.context.get('show_payment_journal_id'):
|
||||
return [(method.id, "%s (%s)" % (method.name, method.journal_id.name)) for method in self]
|
||||
return super().name_get()
|
||||
|
||||
@api.depends('payment_method_id.name')
|
||||
def _compute_name(self):
|
||||
for method in self:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_account_payment_method_line_tree" model="ir.ui.view">
|
||||
<field name="name">account.payment.method.line.tree</field>
|
||||
<field name="model">account.payment.method.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree edit="0">
|
||||
<field name="name" string="Payment Method Name"/>
|
||||
<field name="journal_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -227,7 +227,9 @@
|
||||
</button>
|
||||
|
||||
<!-- Journal Entry button -->
|
||||
<button name="button_open_journal_entry" type="object" class="oe_stat_button" icon="fa-bars">
|
||||
<button name="button_open_journal_entry" type="object"
|
||||
class="oe_stat_button" icon="fa-bars"
|
||||
groups="account.group_account_user,account.group_account_readonly">
|
||||
<div class="o_stat_info">
|
||||
<span class="o_stat_text">Journal Entry</span>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,7 @@ This module also uses analytic accounting and is compatible with the invoice on
|
||||
'security/ir_rule.xml',
|
||||
'report/hr_expense_report.xml',
|
||||
'views/account_move_views.xml',
|
||||
'views/account_payment_views.xml',
|
||||
'views/hr_department_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/account_journal_dashboard.xml',
|
||||
|
||||
@@ -20,5 +20,17 @@
|
||||
<field name="description">Expense report paid</field>
|
||||
<field name="default" eval="True"/>
|
||||
</record>
|
||||
<record id="mt_expense_reset" model="mail.message.subtype">
|
||||
<field name="name">Draft</field>
|
||||
<field name="res_model">hr.expense.sheet</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">Expense report reset to Draft</field>
|
||||
</record>
|
||||
<record id="mt_expense_entry_delete" model="mail.message.subtype">
|
||||
<field name="name">Journal Entry Deleted</field>
|
||||
<field name="res_model">hr.expense.sheet</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">Journal entry deleted</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<template id="hr_expense_template_refuse_reason">
|
||||
<p>Your Expense <t t-if="is_sheet">Report </t><t t-esc="name"/> has been refused</p>
|
||||
<p>Your Expense Report <t t-esc="name"/> has been refused</p>
|
||||
<ul class="o_timeline_tracking_value_list">
|
||||
<li>Reason : <t t-esc="reason"/></li>
|
||||
</ul>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from . import hr_employee
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import account_payment
|
||||
from . import hr_department
|
||||
from . import hr_expense
|
||||
from . import product_template
|
||||
|
||||
@@ -28,45 +28,42 @@ class AccountMove(models.Model):
|
||||
def is_purchase_document(self, include_receipts=False):
|
||||
return bool(self.expense_sheet_id and include_receipts) or super().is_purchase_document(include_receipts)
|
||||
|
||||
def is_entry(self):
|
||||
if self.expense_sheet_id:
|
||||
return False
|
||||
return super().is_entry()
|
||||
|
||||
# Expenses can be written on journal other than purchase, hence don't include them in the constraint check
|
||||
def _check_journal_move_type(self):
|
||||
return super(AccountMove, self.filtered(lambda x: not x.expense_sheet_id))._check_journal_move_type()
|
||||
|
||||
def _creation_message(self):
|
||||
if self.line_ids.expense_id:
|
||||
return _("Expense entry Created")
|
||||
if self.expense_sheet_id:
|
||||
return _("Expense entry created from: %s", self.expense_sheet_id._get_html_link())
|
||||
return super()._creation_message()
|
||||
|
||||
@api.depends('expense_sheet_id.payment_mode')
|
||||
def _compute_payment_state(self):
|
||||
company_paid = self.filtered(lambda m: m.expense_sheet_id.payment_mode == 'company_account')
|
||||
for move in company_paid:
|
||||
move.payment_state = move._get_invoice_in_payment_state()
|
||||
super(AccountMove, self - company_paid)._compute_payment_state()
|
||||
|
||||
@api.depends('expense_sheet_id')
|
||||
def _compute_needed_terms(self):
|
||||
# EXTENDS account
|
||||
# The needed terms need to be computed for journal entries, depending on the expense and the currency
|
||||
# since one expense sheet can contain multiple currencies.
|
||||
super()._compute_needed_terms()
|
||||
for move in self:
|
||||
if move.expense_sheet_id:
|
||||
move.needed_terms = {}
|
||||
agg = defaultdict(lambda: {'company': 0.0, 'foreign': 0.0})
|
||||
for line in move.line_ids:
|
||||
if line.display_type != 'payment_term':
|
||||
agg[line.expense_id]['company'] += line.balance
|
||||
agg[line.expense_id]['foreign'] += line.amount_currency
|
||||
for expense in move.line_ids.expense_id:
|
||||
move.needed_terms[frozendict({
|
||||
'move_id': move.id,
|
||||
'date_maturity': expense.sheet_id.accounting_date or expense.date or fields.Date.context_today(expense),
|
||||
'expense_id': expense.id,
|
||||
})] = {
|
||||
'balance': -agg[expense]['company'],
|
||||
'amount_currency': -agg[expense]['foreign'],
|
||||
'name': '',
|
||||
'currency_id': expense.currency_id.id,
|
||||
'account_id': expense._get_expense_account_destination(),
|
||||
move.needed_terms = {
|
||||
frozendict(
|
||||
{
|
||||
"move_id": move.id,
|
||||
"date_maturity": move.expense_sheet_id.accounting_date
|
||||
or fields.Date.context_today(move.expense_sheet_id),
|
||||
}
|
||||
): {
|
||||
"balance": -sum(move.line_ids.mapped("balance")),
|
||||
"name": "",
|
||||
"account_id": move.expense_sheet_id._get_expense_account_destination(),
|
||||
}
|
||||
}
|
||||
|
||||
def _reverse_moves(self, default_values_list=None, cancel=False):
|
||||
if self.expense_sheet_id:
|
||||
self.expense_sheet_id = False
|
||||
self.ref = False # else, when restarting the expense flow we get duplicate issue on vendor.bill
|
||||
return super()._reverse_moves(default_values_list=default_values_list, cancel=cancel)
|
||||
|
||||
@@ -8,22 +8,11 @@ from odoo.tools.misc import frozendict
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
expense_id = fields.Many2one('hr.expense', string='Expense', copy=False)
|
||||
expense_id = fields.Many2one('hr.expense', string='Expense', copy=True) # copy=True, else we don't know price is tax incl.
|
||||
|
||||
@api.constrains('account_id', 'display_type')
|
||||
def _check_payable_receivable(self):
|
||||
super(AccountMoveLine, self.filtered(lambda line: not line.expense_id or line.expense_id.payment_mode != 'company_account'))._check_payable_receivable()
|
||||
|
||||
def reconcile(self):
|
||||
# OVERRIDE
|
||||
not_paid_expenses = self.expense_id.filtered(lambda expense: expense.state != 'done')
|
||||
res = super().reconcile()
|
||||
# Do not update expense or expense sheet states when reversing journal entries
|
||||
not_paid_expense_sheets = not_paid_expenses.sheet_id.filtered(lambda sheet: sheet.account_move_id.payment_state != 'reversed')
|
||||
paid_expenses = not_paid_expenses.filtered(lambda expense: expense.currency_id.is_zero(expense.amount_residual))
|
||||
paid_expenses.write({'state': 'done'})
|
||||
not_paid_expense_sheets.filtered(lambda sheet: all(expense.state == 'done' for expense in sheet.expense_line_ids)).set_to_paid()
|
||||
return res
|
||||
super(AccountMoveLine, self.filtered(lambda line: line.move_id.expense_sheet_id.payment_mode != 'company_account'))._check_payable_receivable()
|
||||
|
||||
def _get_attachment_domains(self):
|
||||
attachment_domains = super(AccountMoveLine, self)._get_attachment_domains()
|
||||
@@ -59,7 +48,7 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
def _convert_to_tax_base_line_dict(self):
|
||||
result = super()._convert_to_tax_base_line_dict()
|
||||
if self.move_id.expense_sheet_id:
|
||||
if self.expense_id:
|
||||
result.setdefault('extra_context', {})
|
||||
result['extra_context']['force_price_include'] = True
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = "account.payment"
|
||||
|
||||
def action_open_expense_report(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': self.expense_sheet_id.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'views': [(False, 'form')],
|
||||
'res_model': 'hr.expense.sheet',
|
||||
'res_id': self.expense_sheet_id.id
|
||||
}
|
||||
|
||||
def _synchronize_from_moves(self, changed_fields):
|
||||
if self.expense_sheet_id:
|
||||
# Constraints bypass when entry is linked to an expense.
|
||||
# Context is not enough, as we want to be able to delete
|
||||
# and update those entries later on.
|
||||
return
|
||||
return super()._synchronize_from_moves(changed_fields)
|
||||
|
||||
def _synchronize_to_moves(self, changed_fields):
|
||||
if self.expense_sheet_id:
|
||||
raise UserError(_("You cannot do this modification since the payment is linked to an expense report."))
|
||||
return super()._synchronize_to_moves(changed_fields)
|
||||
|
||||
def _creation_message(self):
|
||||
self.ensure_one()
|
||||
if self.move_id.expense_sheet_id:
|
||||
return _("Payment created for: %s", self.move_id.expense_sheet_id._get_html_link())
|
||||
return super()._creation_message()
|
||||
@@ -8,8 +8,8 @@ import werkzeug
|
||||
from odoo import api, fields, Command, models, _
|
||||
from odoo.tools import float_round
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import email_split, float_is_zero, float_repr, float_compare, is_html_empty
|
||||
from odoo.tools.misc import clean_context, format_date
|
||||
from odoo.tools import email_split, float_repr, float_compare, is_html_empty
|
||||
|
||||
|
||||
class HrExpense(models.Model):
|
||||
@@ -100,7 +100,6 @@ class HrExpense(models.Model):
|
||||
approved_by = fields.Many2one('res.users', string='Approved By', related='sheet_id.user_id', tracking=False)
|
||||
approved_on = fields.Datetime(string='Approved On', related='sheet_id.approval_date')
|
||||
reference = fields.Char("Bill Reference")
|
||||
is_refused = fields.Boolean("Explicitly Refused by manager or accountant", readonly=True, copy=False)
|
||||
|
||||
is_editable = fields.Boolean("Is Editable By Current User", compute='_compute_is_editable')
|
||||
is_ref_editable = fields.Boolean("Reference Is Editable By Current User", compute='_compute_is_ref_editable')
|
||||
@@ -571,79 +570,21 @@ class HrExpense(models.Model):
|
||||
# Business
|
||||
# ----------------------------------------
|
||||
|
||||
def _get_expense_account_destination(self):
|
||||
def _prepare_move_line_vals(self):
|
||||
self.ensure_one()
|
||||
account_dest = self.env['account.account']
|
||||
if self.payment_mode == 'company_account':
|
||||
journal = self.sheet_id.bank_journal_id
|
||||
account_dest = (
|
||||
journal.outbound_payment_method_line_ids[0].payment_account_id
|
||||
or journal.company_id.account_journal_payment_credit_account_id
|
||||
)
|
||||
else:
|
||||
if not self.employee_id.sudo().address_home_id:
|
||||
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (self.employee_id.name))
|
||||
partner = self.employee_id.sudo().address_home_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
|
||||
|
||||
def action_move_create(self):
|
||||
'''
|
||||
main function that is called when trying to create the accounting entries related to an expense
|
||||
'''
|
||||
moves = self.env['account.move'].create([
|
||||
{
|
||||
'journal_id': (
|
||||
sheet.bank_journal_id
|
||||
if sheet.payment_mode == 'company_account' else
|
||||
sheet.journal_id
|
||||
).id,
|
||||
'move_type': 'in_receipt',
|
||||
'company_id': sheet.company_id.id,
|
||||
'partner_id': sheet.employee_id.sudo().address_home_id.commercial_partner_id.id,
|
||||
'date': sheet.accounting_date or fields.Date.context_today(sheet),
|
||||
'invoice_date': sheet.accounting_date or fields.Date.context_today(sheet),
|
||||
'ref': sheet.name,
|
||||
# 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': '/',
|
||||
'expense_sheet_id': [Command.set(sheet.ids)],
|
||||
'line_ids':[
|
||||
Command.create({
|
||||
'name': expense.employee_id.name + ': ' + expense.name.split('\n')[0][:64],
|
||||
'account_id': expense.account_id.id,
|
||||
'quantity': expense.quantity or 1,
|
||||
'price_unit': expense.unit_amount if expense.unit_amount != 0 else expense.total_amount,
|
||||
'product_id': expense.product_id.id,
|
||||
'product_uom_id': expense.product_uom_id.id,
|
||||
'analytic_distribution': expense.analytic_distribution,
|
||||
'expense_id': expense.id,
|
||||
'partner_id': expense.employee_id.sudo().address_home_id.commercial_partner_id.id,
|
||||
'tax_ids': [(6, 0, expense.tax_ids.ids)],
|
||||
'currency_id': expense.currency_id.id,
|
||||
})
|
||||
for expense in sheet.expense_line_ids
|
||||
]
|
||||
}
|
||||
for sheet in self.sheet_id
|
||||
])
|
||||
moves._post()
|
||||
|
||||
for expense in self:
|
||||
if expense.payment_mode == 'company_account':
|
||||
expense.sheet_id.paid_expense_sheets()
|
||||
|
||||
return {move.expense_sheet_id.id: move for move in moves}
|
||||
|
||||
def refuse_expense(self, reason):
|
||||
self.write({'is_refused': True})
|
||||
self.sheet_id.write({'state': 'cancel'})
|
||||
for expense in self:
|
||||
expense.sheet_id.message_post_with_source(
|
||||
'hr_expense.hr_expense_template_refuse_reason',
|
||||
render_values={'reason': reason, 'is_sheet': False, 'name': expense.name},
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
return {
|
||||
'name': self.employee_id.name + ': ' + self.name.split('\n')[0][:64],
|
||||
'account_id': self.account_id.id,
|
||||
'quantity': self.quantity or 1,
|
||||
'price_unit': self.unit_amount if self.unit_amount != 0 else self.total_amount,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'analytic_distribution': self.analytic_distribution,
|
||||
'expense_id': self.id,
|
||||
'partner_id': False if self.payment_mode == 'company_account' else self.employee_id.sudo().address_home_id.commercial_partner_id.id,
|
||||
'tax_ids': [Command.set(self.tax_ids.ids)],
|
||||
'currency_id': self.currency_id.id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_expense_dashboard(self):
|
||||
@@ -862,19 +803,9 @@ class HrExpenseSheet(models.Model):
|
||||
journal = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', default_company_id)], limit=1)
|
||||
return journal.id
|
||||
|
||||
@api.model
|
||||
def _default_bank_journal_id(self):
|
||||
company_journal_id = self.env.company.company_expense_journal_id
|
||||
if company_journal_id:
|
||||
return company_journal_id
|
||||
default_company_id = self.default_get(['company_id'])['company_id']
|
||||
journal = self.env['account.journal'].search([('type', 'in', ['cash', 'bank']), ('company_id', '=', default_company_id)], limit=1)
|
||||
return journal
|
||||
|
||||
name = fields.Char('Expense Report Summary', required=True, tracking=True)
|
||||
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', copy=False)
|
||||
product_ids = fields.Many2many('product.product', compute='_compute_product_ids', search='_search_product_ids', string='Categories')
|
||||
is_editable = fields.Boolean("Expense Lines Are Editable By Current User", compute='_compute_is_editable')
|
||||
expense_number = fields.Integer(compute='_compute_expense_number', string='Number of Expenses')
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
@@ -883,15 +814,21 @@ class HrExpenseSheet(models.Model):
|
||||
('post', 'Posted'),
|
||||
('done', 'Done'),
|
||||
('cancel', 'Refused')
|
||||
], string='Status', index=True, readonly=True, tracking=True, copy=False, default='draft', required=True)
|
||||
payment_state = fields.Selection(
|
||||
selection=lambda self: self.env["account.move"]._fields["payment_state"].selection,
|
||||
string="Payment Status",
|
||||
store=True, readonly=True, copy=False, tracking=True, compute='_compute_payment_state')
|
||||
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, tracking=True, states={'draft': [('readonly', False)]}, default=_default_employee_id, check_company=True, domain= lambda self: self.env['hr.expense']._get_employee_id_domain())
|
||||
], compute='_compute_state', string='Status', index=True, readonly=True, store=True, tracking=True, copy=False, default='draft', required=True)
|
||||
approval_state = fields.Selection([
|
||||
('submit', 'Submitted'),
|
||||
('approve', 'Approved'),
|
||||
('cancel', 'Refused'),
|
||||
], copy=False)
|
||||
approval_date = fields.Datetime('Approval Date', readonly=True)
|
||||
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.company)
|
||||
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, tracking=True, states={'draft': [('readonly', False)]}, default=_default_employee_id, check_company=True, domain=lambda self: self.env['hr.expense']._get_employee_id_domain())
|
||||
address_id = fields.Many2one('res.partner', compute='_compute_from_employee_id', store=True, readonly=False, copy=True, string="Employee Home Address", check_company=True)
|
||||
payment_mode = fields.Selection(related='expense_line_ids.payment_mode', readonly=True, string="Paid By", tracking=True)
|
||||
department_id = fields.Many2one('hr.department', compute='_compute_from_employee_id', store=True, readonly=False, copy=False, string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
|
||||
user_id = fields.Many2one('res.users', 'Manager', compute='_compute_from_employee_id', store=True, readonly=True, copy=False, states={'draft': [('readonly', False)]}, tracking=True, domain=lambda self: [('groups_id', 'in', self.env.ref('hr_expense.group_hr_expense_team_approver').id)])
|
||||
|
||||
# === Amount fields === #
|
||||
total_amount = fields.Monetary('Total Amount', currency_field='currency_id', compute='_compute_amount', store=True, tracking=True)
|
||||
untaxed_amount = fields.Monetary('Untaxed Amount', currency_field='currency_id', compute='_compute_amount', store=True)
|
||||
total_amount_taxes = fields.Monetary('Taxes', currency_field='currency_id', compute='_compute_amount', store=True)
|
||||
@@ -899,21 +836,44 @@ class HrExpenseSheet(models.Model):
|
||||
amount_residual = fields.Monetary(
|
||||
string="Amount Due", store=True,
|
||||
currency_field='currency_id',
|
||||
related='account_move_id.amount_residual')
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.company)
|
||||
compute='_compute_from_account_move_id')
|
||||
|
||||
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.company.currency_id)
|
||||
attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments')
|
||||
journal_id = fields.Many2one('account.journal', string='Expense Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, check_company=True, domain="[('type', '=', 'purchase'), ('company_id', '=', company_id)]",
|
||||
default=_default_journal_id, help="The journal used when the expense is done.")
|
||||
bank_journal_id = fields.Many2one('account.journal', string='Bank Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, check_company=True, domain="[('type', 'in', ['cash', 'bank']), ('company_id', '=', company_id)]",
|
||||
default=_default_bank_journal_id, help="The payment method used when the expense is paid by the company.")
|
||||
accounting_date = fields.Date("Accounting Date")
|
||||
account_move_id = fields.Many2one('account.move', string='Journal Entry', ondelete='restrict', copy=False, readonly=True)
|
||||
department_id = fields.Many2one('hr.department', compute='_compute_from_employee_id', store=True, readonly=False, copy=False, string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
|
||||
is_multiple_currency = fields.Boolean("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",
|
||||
copy=False,
|
||||
tracking=True,
|
||||
store=True, readonly=True, compute='_compute_from_account_move_id')
|
||||
payment_mode = fields.Selection(
|
||||
string="Paid By",
|
||||
tracking=True,
|
||||
related='expense_line_ids.payment_mode', readonly=True)
|
||||
employee_journal_id = fields.Many2one(
|
||||
'account.journal', string='Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, check_company=True, domain="[('type', '=', 'purchase'), ('company_id', '=', company_id)]",
|
||||
default=_default_journal_id, help="The journal used when the expense is paid by employee.")
|
||||
selectable_payment_method_line_ids = fields.Many2many('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",
|
||||
domain="[('id', 'in', selectable_payment_method_line_ids)]",
|
||||
compute='_compute_payment_method_line_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
help="The payment method used when the expense is paid by the company.",
|
||||
)
|
||||
accounting_date = fields.Date("Accounting Date")
|
||||
account_move_id = fields.Many2one('account.move', string='Journal Entry', ondelete='set null', copy=False, readonly=True)
|
||||
journal_id = fields.Many2one('account.journal', compute='_compute_journal_id', string="Expense Journal", store=True)
|
||||
|
||||
# === Security fields === #
|
||||
can_reset = fields.Boolean('Can Reset', compute='_compute_can_reset')
|
||||
can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
|
||||
approval_date = fields.Datetime('Approval Date', readonly=True)
|
||||
cannot_approve_reason = fields.Char('Cannot Approve Reason', compute='_compute_can_approve')
|
||||
is_editable = fields.Boolean("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'),
|
||||
@@ -926,14 +886,47 @@ class HrExpenseSheet(models.Model):
|
||||
sheet.total_amount_taxes = sum(sheet.expense_line_ids.mapped('amount_tax_company'))
|
||||
sheet.untaxed_amount = sheet.total_amount - sheet.total_amount_taxes
|
||||
|
||||
@api.depends('account_move_id.payment_state')
|
||||
def _compute_payment_state(self):
|
||||
@api.depends('account_move_id.payment_state', 'account_move_id.amount_residual')
|
||||
def _compute_from_account_move_id(self):
|
||||
for sheet in self:
|
||||
sheet.payment_state = sheet.account_move_id.payment_state or 'not_paid'
|
||||
amount = sheet.account_move_id.amount_residual
|
||||
state = sheet.account_move_id.payment_state or 'not_paid'
|
||||
# when the sheet is paid by the company, the state/amount of
|
||||
# the related account_move_id is not relevant
|
||||
if sheet.account_move_id and sheet.payment_mode == 'company_account':
|
||||
state = 'paid'
|
||||
amount = 0.0
|
||||
sheet.payment_state = state
|
||||
sheet.amount_residual = amount
|
||||
|
||||
def _compute_attachment_number(self):
|
||||
@api.depends('selectable_payment_method_line_ids')
|
||||
def _compute_payment_method_line_id(self):
|
||||
for sheet in self:
|
||||
sheet.attachment_number = sum(sheet.expense_line_ids.mapped('attachment_number'))
|
||||
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:
|
||||
sheet.journal_id = sheet.payment_method_line_id.journal_id if sheet.payment_mode == 'company_account'\
|
||||
else sheet.employee_journal_id
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_selectable_payment_method_line_ids(self):
|
||||
for sheet in self:
|
||||
sheet.selectable_payment_method_line_ids = sheet.company_id.company_expense_allowed_payment_method_line_ids\
|
||||
or self.env['account.payment.method.line'].search([('payment_type', '=', 'outbound'), ('company_id', '=', sheet.company_id.id)])
|
||||
|
||||
@api.depends('account_move_id', '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_id:
|
||||
sheet.state = 'post'
|
||||
elif sheet.approval_state:
|
||||
sheet.state = sheet.approval_state
|
||||
else:
|
||||
sheet.state = 'draft'
|
||||
|
||||
@api.depends('expense_line_ids.currency_id')
|
||||
def _compute_is_multiple_currency(self):
|
||||
@@ -949,10 +942,26 @@ class HrExpenseSheet(models.Model):
|
||||
@api.depends_context('uid')
|
||||
@api.depends('employee_id')
|
||||
def _compute_can_approve(self):
|
||||
is_approver = self.user_has_groups('hr_expense.group_hr_expense_team_approver, hr_expense.group_hr_expense_user')
|
||||
is_manager = self.user_has_groups('hr_expense.group_hr_expense_manager')
|
||||
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:
|
||||
sheet.can_approve = is_manager or (is_approver and sheet.employee_id.user_id != self.env.user)
|
||||
reason = False
|
||||
if not is_team_approver:
|
||||
reason = _("%s: Your are not a Manager or HR Officer", sheet.name)
|
||||
|
||||
elif not is_hr_admin:
|
||||
current_managers = sheet.employee_id.expense_manager_id | sheet.employee_id.parent_id.user_id | sheet.employee_id.department_id.manager_id.user_id | sheet.user_id
|
||||
|
||||
if self.employee_id.user_id == self.env.user:
|
||||
reason = _("%s: It is your own expense", sheet.name)
|
||||
|
||||
elif not self.env.user in current_managers and not is_approver and sheet.employee_id.expense_manager_id != self.env.user:
|
||||
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_expense_number(self):
|
||||
@@ -1012,6 +1021,19 @@ class HrExpenseSheet(models.Model):
|
||||
value = [value]
|
||||
return [('expense_line_ids.product_id', operator, value)]
|
||||
|
||||
# ----------------------------------------
|
||||
# ORM Overrides
|
||||
# ----------------------------------------
|
||||
|
||||
def read(self, fields=None, load='_classic_read'):
|
||||
# setting the context in the field on the view is not enough
|
||||
return super(HrExpenseSheet, self.with_context(show_payment_journal_id=True)).read(fields=fields, load=load)
|
||||
|
||||
def onchange(self, values, field_name, field_onchange):
|
||||
# setting the context in the field on the view is not enough
|
||||
self_ctx = self.with_context(show_payment_journal_id=True)
|
||||
return super(HrExpenseSheet, self_ctx).onchange(values, field_name, field_onchange)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
context = clean_context(self.env.context)
|
||||
@@ -1046,7 +1068,11 @@ class HrExpenseSheet(models.Model):
|
||||
|
||||
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')
|
||||
elif 'state' in init_values and self.state == 'cancel':
|
||||
return self.env.ref('hr_expense.mt_expense_refused')
|
||||
@@ -1062,190 +1088,6 @@ class HrExpenseSheet(models.Model):
|
||||
res.append((employee.user_id.partner_id.id, subtype_ids, False))
|
||||
return res
|
||||
|
||||
# --------------------------------------------
|
||||
# Actions
|
||||
# --------------------------------------------
|
||||
|
||||
def action_sheet_move_create(self):
|
||||
samples = self.mapped('expense_line_ids.sample')
|
||||
if samples.count(True):
|
||||
if samples.count(False):
|
||||
raise UserError(_("You can't mix sample expenses and regular ones"))
|
||||
self.write({'state': 'post'})
|
||||
return
|
||||
|
||||
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."))
|
||||
|
||||
expense_line_ids = self.mapped('expense_line_ids')\
|
||||
.filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.company.currency_id).rounding))
|
||||
res = expense_line_ids.with_context(clean_context(self.env.context)).action_move_create()
|
||||
for sheet in self.filtered(lambda s: not s.accounting_date):
|
||||
sheet.accounting_date = sheet.account_move_id.date
|
||||
to_post = self.filtered(lambda sheet: sheet.payment_mode == 'own_account' and sheet.expense_line_ids)
|
||||
to_post.write({'state': 'post'})
|
||||
(self - to_post).write({'state': 'done'})
|
||||
self.activity_update()
|
||||
return res
|
||||
|
||||
def action_unpost(self):
|
||||
self = self.with_context(clean_context(self.env.context))
|
||||
for sheet in self:
|
||||
move = sheet.account_move_id
|
||||
sheet.account_move_id = False
|
||||
if move.state == 'draft':
|
||||
move.unlink()
|
||||
else:
|
||||
move._reverse_moves(cancel=True)
|
||||
sheet.write({'state': 'draft'})
|
||||
|
||||
def action_get_attachment_view(self):
|
||||
res = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
|
||||
res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.expense_line_ids.ids)]
|
||||
res['context'] = {
|
||||
'default_res_model': 'hr.expense.sheet',
|
||||
'default_res_id': self.id,
|
||||
'create': False,
|
||||
'edit': False,
|
||||
}
|
||||
return res
|
||||
|
||||
def action_get_expense_view(self):
|
||||
self.ensure_one()
|
||||
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_move(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': self.account_move_id.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'views': [[False, "form"]],
|
||||
'res_model': 'account.move',
|
||||
'res_id': self.account_move_id.id
|
||||
}
|
||||
|
||||
# --------------------------------------------
|
||||
# Business
|
||||
# --------------------------------------------
|
||||
|
||||
def set_to_paid(self):
|
||||
self.write({'state': 'done'})
|
||||
|
||||
def action_submit_sheet(self):
|
||||
self.write({'state': 'submit'})
|
||||
self.sudo().activity_update()
|
||||
|
||||
def _check_can_approve(self):
|
||||
if not self.user_has_groups('hr_expense.group_hr_expense_team_approver'):
|
||||
raise UserError(_("Only Managers and HR Officers can approve expenses"))
|
||||
elif not self.user_has_groups('hr_expense.group_hr_expense_manager'):
|
||||
current_managers = self.employee_id.expense_manager_id | self.employee_id.parent_id.user_id | self.employee_id.department_id.manager_id.user_id | self.user_id
|
||||
|
||||
if self.employee_id.user_id == self.env.user:
|
||||
raise UserError(_("You cannot approve your own expenses"))
|
||||
|
||||
if not self.env.user in current_managers and not self.user_has_groups('hr_expense.group_hr_expense_user') and self.employee_id.expense_manager_id != self.env.user:
|
||||
raise UserError(_("You can only approve your department expenses"))
|
||||
|
||||
def 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 _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 _do_approve(self):
|
||||
self._check_can_approve()
|
||||
|
||||
notification = {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('There are no expense reports to approve.'),
|
||||
'type': 'warning',
|
||||
'sticky': False, #True/False will display for few seconds if false
|
||||
},
|
||||
}
|
||||
|
||||
filtered_sheet = self.filtered(lambda s: s.state in ['submit', 'draft'])
|
||||
if not filtered_sheet:
|
||||
return notification
|
||||
for sheet in filtered_sheet:
|
||||
sheet.write({'state': 'approve', 'user_id': sheet.user_id.id or self.env.user.id})
|
||||
notification['params'].update({
|
||||
'title': _('The expense reports were successfully approved.'),
|
||||
'type': 'success',
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
})
|
||||
|
||||
self.activity_update()
|
||||
return notification
|
||||
|
||||
def paid_expense_sheets(self):
|
||||
self.write({'state': 'done'})
|
||||
|
||||
def refuse_sheet(self, reason):
|
||||
if not self.user_has_groups('hr_expense.group_hr_expense_team_approver'):
|
||||
raise UserError(_("Only Managers and HR Officers can approve expenses"))
|
||||
elif not self.user_has_groups('hr_expense.group_hr_expense_manager'):
|
||||
current_managers = self.employee_id.expense_manager_id | self.employee_id.parent_id.user_id | self.employee_id.department_id.manager_id.user_id | self.user_id
|
||||
|
||||
if self.employee_id.user_id == self.env.user:
|
||||
raise UserError(_("You cannot refuse your own expenses"))
|
||||
|
||||
if not self.env.user in current_managers and not self.user_has_groups('hr_expense.group_hr_expense_user') and self.employee_id.expense_manager_id != self.env.user:
|
||||
raise UserError(_("You can only refuse your department expenses"))
|
||||
|
||||
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, 'is_sheet': True, 'name': sheet.name},
|
||||
)
|
||||
self.activity_update()
|
||||
|
||||
def reset_expense_sheets(self):
|
||||
if not self.can_reset:
|
||||
raise UserError(_("Only HR Officers or the concerned employee can reset to draft."))
|
||||
self.mapped('expense_line_ids').write({'is_refused': False})
|
||||
self.sudo().write({'state': 'draft', 'approval_date': False})
|
||||
self.activity_update()
|
||||
return True
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
if self.user_id:
|
||||
return self.user_id
|
||||
elif self.employee_id.parent_id.user_id:
|
||||
return self.employee_id.parent_id.user_id
|
||||
elif 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 activity_update(self):
|
||||
reports_requiring_feedback = self.env['hr.expense.sheet']
|
||||
reports_activity_unlink = self.env['hr.expense.sheet']
|
||||
@@ -1263,20 +1105,226 @@ class HrExpenseSheet(models.Model):
|
||||
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()
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id('hr_expense.hr_expense_refuse_wizard_action')
|
||||
return 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.
|
||||
:return: An action opening the account.payment.register wizard.
|
||||
'''
|
||||
return self.account_move_id.with_context(
|
||||
default_partner_bank_id=self.employee_id.sudo().bank_account_id.id
|
||||
).action_register_payment()
|
||||
|
||||
def action_open_expense_view(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Register Payment'),
|
||||
'res_model': 'account.payment.register',
|
||||
'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_move(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': self.account_move_id.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'views': [[False, "form"]],
|
||||
'context': {
|
||||
'active_model': 'account.move',
|
||||
'active_ids': self.account_move_id.ids,
|
||||
'default_partner_bank_id': self.employee_id.sudo().bank_account_id.id,
|
||||
},
|
||||
'target': 'new',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move' if self.payment_mode == 'own_account' else 'account.payment',
|
||||
'res_id': self.account_move_id.id if self.payment_mode == 'own_account' else self.account_move_id.payment_id.id,
|
||||
}
|
||||
|
||||
# --------------------------------------------
|
||||
# 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."))
|
||||
|
||||
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_*
|
||||
|
||||
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_bill_vals() for sheet in own_account_sheets])
|
||||
payments = self.env['account.payment'].create([sheet._prepare_payment_vals() for sheet in company_account_sheets])
|
||||
|
||||
moves |= payments.move_id
|
||||
moves.action_post()
|
||||
|
||||
self.activity_update()
|
||||
|
||||
for sheet in self.filtered(lambda s: not s.accounting_date):
|
||||
sheet.accounting_date = sheet.account_move_id.date
|
||||
|
||||
return {move.expense_sheet_id.id: move for move in moves}
|
||||
|
||||
def _do_reverse_moves(self):
|
||||
draft_moves = self.account_move_id.filtered(lambda _move: _move.state == 'draft')
|
||||
draft_moves.unlink()
|
||||
moves = self.account_move_id - 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_payment_vals(self):
|
||||
self.ensure_one()
|
||||
res = self._prepare_move_vals()
|
||||
res.update({
|
||||
'move_type': 'entry',
|
||||
'amount': self.total_amount,
|
||||
'payment_type': 'outbound',
|
||||
'partner_type': 'supplier',
|
||||
'payment_method_line_id': self.payment_method_line_id.id,
|
||||
'partner_id': False,
|
||||
})
|
||||
return res
|
||||
|
||||
def _prepare_bill_vals(self):
|
||||
self.ensure_one()
|
||||
res = self._prepare_move_vals()
|
||||
res.update({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.employee_id.sudo().address_home_id.commercial_partner_id.id,
|
||||
})
|
||||
return res
|
||||
|
||||
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': '/',
|
||||
'journal_id': self.journal_id.id,
|
||||
'date': self.accounting_date or fields.Date.context_today(self),
|
||||
'invoice_date': self.accounting_date or fields.Date.context_today(self), # expense payment behave as bills
|
||||
'ref': self.name,
|
||||
'expense_sheet_id': [Command.set(self.ids)],
|
||||
'line_ids':[
|
||||
Command.create(expense._prepare_move_line_vals())
|
||||
for expense in self.expense_line_ids
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
elif self.employee_id.parent_id.user_id:
|
||||
return self.employee_id.parent_id.user_id
|
||||
elif 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()
|
||||
account_dest = self.env['account.account']
|
||||
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().address_home_id:
|
||||
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (self.employee_id.name))
|
||||
partner = self.employee_id.sudo().address_home_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
|
||||
|
||||
@@ -13,10 +13,9 @@ class ResCompany(models.Model):
|
||||
domain="[('type', '=', 'purchase'), ('company_id', '=', company_id)]",
|
||||
help="The company's default journal used when an employee expense is created.",
|
||||
)
|
||||
company_expense_journal_id = fields.Many2one(
|
||||
"account.journal",
|
||||
string="Default Company Expense Journal",
|
||||
company_expense_allowed_payment_method_line_ids = fields.Many2many(
|
||||
"account.payment.method.line",
|
||||
string="Payment methods available for expenses paid by company",
|
||||
check_company=True,
|
||||
domain="[('type', 'in', ['cash', 'bank']), ('company_id', '=', company_id)]",
|
||||
help="The company's default journal used when a company expense is created.",
|
||||
domain="[('payment_type', '=', 'outbound'), ('company_id', '=', company_id),('journal_id', '!=', False)]",
|
||||
)
|
||||
|
||||
@@ -16,7 +16,11 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_hr_payroll_expense = fields.Boolean(string='Reimburse Expenses in Payslip')
|
||||
module_hr_expense_extract = fields.Boolean(string='Send bills to OCR to generate expenses')
|
||||
expense_journal_id = fields.Many2one('account.journal', related='company_id.expense_journal_id', readonly=False)
|
||||
company_expense_journal_id = fields.Many2one('account.journal', related='company_id.company_expense_journal_id', readonly=False)
|
||||
company_expense_allowed_payment_method_line_ids = fields.Many2many(
|
||||
comodel_name='account.payment.method.line',
|
||||
related='company_id.company_expense_allowed_payment_method_line_ids',
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
|
||||
@@ -54,15 +54,17 @@ export class ExpenseListController extends ListController {
|
||||
const recordIds = records.map((a) => a.resId);
|
||||
const model = this.model.rootParams.resModel;
|
||||
const res = await this.orm.call(model, action, [recordIds]);
|
||||
await this.actionService.doAction(res, {
|
||||
additionalContext: {
|
||||
dont_redirect_to_payments: 1,
|
||||
},
|
||||
onClose: async () => {
|
||||
await this.model.root.load();
|
||||
this.render(true);
|
||||
}
|
||||
});
|
||||
if (res) {
|
||||
await this.actionService.doAction(res, {
|
||||
additionalContext: {
|
||||
dont_redirect_to_payments: 1,
|
||||
},
|
||||
onClose: async () => {
|
||||
await this.model.root.load();
|
||||
this.render(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
// sgv note: we tried this.model.notify(); and does not work
|
||||
await this.model.root.load();
|
||||
this.render(true);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
|
||||
<button t-if="displayApprove()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('approve_expense_sheets')">
|
||||
<button t-if="displayApprove()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_approve_expense_sheets')">
|
||||
Approve Report
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
@@ -48,8 +48,8 @@ class TestExpenses(TestExpenseCommon):
|
||||
expense2.employee_id = self.expense_employee.id
|
||||
self.assertEqual(expense2.sheet_id.id, False, 'Sheet should be unlinked from the expense')
|
||||
|
||||
def test_expense_sheet_payment_state(self):
|
||||
''' Test expense sheet payment states when partially paid, in payment and paid. '''
|
||||
def test_expense_sheet_paid_employee(self):
|
||||
''' Test expense sheet paid by employee flow'''
|
||||
|
||||
def get_payment(expense_sheet, amount):
|
||||
ctx = {'active_model': 'account.move', 'active_ids': expense_sheet.account_move_id.ids}
|
||||
@@ -73,19 +73,45 @@ class TestExpenses(TestExpenseCommon):
|
||||
})
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.approve_expense_sheets()
|
||||
self.assertEqual(expense_sheet.state, 'submit', 'sheet should be submitted')
|
||||
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
self.assertEqual(expense_sheet.state, 'approve', 'sheet should be aproved')
|
||||
|
||||
expense_sheet.action_sheet_move_create()
|
||||
self.assertEqual(expense_sheet.state, 'post', 'sheet should be posted')
|
||||
|
||||
move = expense_sheet.account_move_id
|
||||
|
||||
self.assertRecordValues(move.line_ids, [
|
||||
{'debit': 304.35, 'credit': 0.0, 'reconciled': False, },
|
||||
{'debit': 45.65, 'credit': 0.0, 'reconciled': False},
|
||||
{'debit': 0.0, 'credit': 350, 'reconciled': False},
|
||||
])
|
||||
|
||||
expense_sheet.action_reset_expense_sheets()
|
||||
self.assertEqual(expense_sheet.state, 'draft', 'sheet should be reverted to approved')
|
||||
|
||||
reverse_move = self.env['account.move'].search([('reversed_entry_id', '=', move.id)])
|
||||
self.assertFalse(expense_sheet.account_move_id)
|
||||
self.assertEqual(move.payment_state, 'reversed', 'vendor bill should be reversed')
|
||||
self.assertTrue(350 == expense_sheet.total_amount == move.amount_total == reverse_move.amount_total, 'taxes properly included in price')
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
payment = get_payment(expense_sheet, 100.0)
|
||||
liquidity_lines1 = payment._seek_for_lines()[0]
|
||||
payment_1 = get_payment(expense_sheet, 100.0)
|
||||
liquidity_lines1 = payment_1._seek_for_lines()[0]
|
||||
|
||||
self.assertEqual(expense_sheet.payment_state, 'partial', 'payment_state should be partial')
|
||||
|
||||
payment = get_payment(expense_sheet, 250.0)
|
||||
liquidity_lines2 = payment._seek_for_lines()[0]
|
||||
payment_2 = get_payment(expense_sheet, 250.0)
|
||||
liquidity_lines2 = payment_2._seek_for_lines()[0]
|
||||
|
||||
in_payment_state = expense_sheet.account_move_id._get_invoice_in_payment_state()
|
||||
self.assertEqual(expense_sheet.payment_state, in_payment_state, 'payment_state should be ' + in_payment_state)
|
||||
self.assertEqual(expense_sheet.state, 'done', 'sheet should be marked as done')
|
||||
|
||||
statement_line = self.env['account.bank.statement.line'].create({
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
@@ -102,6 +128,47 @@ class TestExpenses(TestExpenseCommon):
|
||||
(st_suspense_lines + liquidity_lines1 + liquidity_lines2).reconcile()
|
||||
|
||||
self.assertEqual(expense_sheet.payment_state, 'paid', 'payment_state should be paid')
|
||||
self.assertEqual(expense_sheet.state, 'done', 'sheet should be marked as done')
|
||||
|
||||
payment_1.action_draft()
|
||||
payment_2.action_draft()
|
||||
|
||||
self.assertEqual(expense_sheet.state, 'post', 'sheet should be reverted to posted')
|
||||
self.assertEqual(expense_sheet.payment_state, 'not_paid', 'payment_state should be not_paid')
|
||||
|
||||
def test_expense_sheet_paid_company(self):
|
||||
''' Test expense sheet paid by company flow '''
|
||||
|
||||
expense_sheet = self.env['hr.expense.sheet'].create({
|
||||
'name': 'Expense for John Smith',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'accounting_date': '2021-01-01',
|
||||
'payment_method_line_id': self.outbound_payment_method_line.id,
|
||||
'expense_line_ids': [(0, 0, {
|
||||
'name': 'Car Travel Expenses',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 350.00,
|
||||
'payment_mode': 'company_account',
|
||||
})]
|
||||
})
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
move = expense_sheet.account_move_id
|
||||
|
||||
self.assertEqual(expense_sheet.state, 'done', 'sheet should be marked as done')
|
||||
self.assertTrue(350 == expense_sheet.total_amount == move.amount_total == move.payment_id.amount)
|
||||
|
||||
self.assertEqual(expense_sheet.payment_state, 'paid', 'payment_state should be paid')
|
||||
|
||||
move.payment_id.action_draft()
|
||||
move.payment_id.unlink()
|
||||
|
||||
self.assertEqual(expense_sheet.state, 'approve', 'sheet should be reverted to approve')
|
||||
self.assertEqual(expense_sheet.payment_state, 'not_paid', 'payment_state should be not_paid')
|
||||
|
||||
def test_expense_values(self):
|
||||
""" Checking accounting move entries and analytic entries when submitting expense """
|
||||
@@ -143,7 +210,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'draft', 'total_amount': 1500.0}])
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
# Check expense sheet journal entry values.
|
||||
@@ -151,25 +218,14 @@ class TestExpenses(TestExpenseCommon):
|
||||
# Receivable line (company currency):
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 1000.0,
|
||||
'amount_currency': -1000.0,
|
||||
'credit': 1750.0,
|
||||
'amount_currency': -1750.0,
|
||||
'account_id': self.company_data['default_account_payable'].id,
|
||||
'product_id': False,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
'tax_line_id': False,
|
||||
'analytic_distribution': False,
|
||||
},
|
||||
# Receivable line (foreign currency):
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 750,
|
||||
'amount_currency': -1500.0,
|
||||
'account_id': self.company_data['default_account_payable'].id,
|
||||
'product_id': False,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'tax_line_id': False,
|
||||
'analytic_distribution': False,
|
||||
},
|
||||
# Tax line (foreign currency):
|
||||
{
|
||||
'debit': 97.83,
|
||||
@@ -248,10 +304,11 @@ class TestExpenses(TestExpenseCommon):
|
||||
expense_sheet_form.employee_id = expense.employee_id
|
||||
expense_sheet_form.name = expense.name
|
||||
expense_sheet_form.expense_line_ids.add(expense)
|
||||
expense_sheet_form.payment_method_line_id = self.outbound_payment_method_line
|
||||
expense_sheet = expense_sheet_form.save()
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
def test_account_entry_multi_currency(self):
|
||||
@@ -285,7 +342,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
expense.action_submit_sheet()
|
||||
self.assertEqual(expense.state, 'submit', 'Expense is not in Reported state')
|
||||
# Approve
|
||||
expense.approve_expense_sheets()
|
||||
expense.action_approve_expense_sheets()
|
||||
self.assertEqual(expense.state, 'approve', 'Expense is not in Approved state')
|
||||
# Create Expense Entries
|
||||
expense.action_sheet_move_create()
|
||||
@@ -312,7 +369,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
'analytic_line_ids': [],
|
||||
}, {
|
||||
'balance': -350.0,
|
||||
'amount_currency': -700.0,
|
||||
'amount_currency': -350.0,
|
||||
'product_id': False,
|
||||
'price_unit': 0.0,
|
||||
'price_subtotal': 0.0,
|
||||
@@ -347,7 +404,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
})
|
||||
|
||||
expense.action_submit_sheet()
|
||||
expense.approve_expense_sheets()
|
||||
expense.action_approve_expense_sheets()
|
||||
|
||||
# Assert not "Cannot create unbalanced journal entry" error.
|
||||
expense.action_sheet_move_create()
|
||||
@@ -398,14 +455,13 @@ class TestExpenses(TestExpenseCommon):
|
||||
|
||||
#actions
|
||||
sheet.action_submit_sheet()
|
||||
sheet.approve_expense_sheets()
|
||||
sheet.action_approve_expense_sheets()
|
||||
sheet.action_sheet_move_create()
|
||||
action_data = sheet.action_register_payment()
|
||||
wizard = Form(self.env['account.payment.register'].with_context(action_data['context'])).save()
|
||||
wizard.group_payment = False
|
||||
action = wizard.action_create_payments()
|
||||
self.assertEqual(sheet.state, 'done', 'all account.move.line linked to expenses must be reconciled after payment')
|
||||
move = self.env['account.payment'].search(action['domain']).move_id
|
||||
move = self.env['account.payment'].browse(action['res_id']).move_id
|
||||
move.button_cancel()
|
||||
self.assertEqual(sheet.state, 'done', 'Sheet state must not change when the payment linked to that sheet is canceled')
|
||||
|
||||
@@ -428,7 +484,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
|
||||
#actions
|
||||
sheet.action_submit_sheet()
|
||||
sheet.approve_expense_sheets()
|
||||
sheet.action_approve_expense_sheets()
|
||||
sheet.action_sheet_move_create()
|
||||
action_data = sheet.action_register_payment()
|
||||
wizard = Form(self.env['account.payment.register'].with_context(action_data['context'])).save()
|
||||
@@ -505,18 +561,14 @@ class TestExpenses(TestExpenseCommon):
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'draft', 'total_amount': 345.0}])
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
# Check expense sheet journal entry values.
|
||||
self.assertRecordValues(expense_sheet.account_move_id.line_ids.sorted('balance'), [
|
||||
# Receivable lines:
|
||||
{
|
||||
'balance': -230.0,
|
||||
'account_id': self.company_data['default_account_payable'].id,
|
||||
},
|
||||
{
|
||||
'balance': -115.0,
|
||||
'balance': -345.0,
|
||||
'account_id': self.company_data['default_account_payable'].id,
|
||||
},
|
||||
# Tax lines:
|
||||
@@ -569,7 +621,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
})
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
# Check whether employee is set as supplier on the receipt
|
||||
@@ -606,31 +658,29 @@ class TestExpenses(TestExpenseCommon):
|
||||
|
||||
#actions
|
||||
sheet.action_submit_sheet()
|
||||
sheet.approve_expense_sheets()
|
||||
sheet.action_approve_expense_sheets()
|
||||
sheet.action_sheet_move_create()
|
||||
action_data = sheet.action_register_payment()
|
||||
payment_method_line = self.env.company.bank_journal_ids.outbound_payment_method_line_ids.filtered(lambda m: m.code == 'check_printing')
|
||||
with Form(self.env[action_data['res_model']].with_context(action_data['context'])) as wiz_form:
|
||||
wiz_form.payment_method_line_id = payment_method_line
|
||||
wiz_form.group_payment = False
|
||||
wizard = wiz_form.save()
|
||||
action = wizard.action_create_payments()
|
||||
self.assertEqual(sheet.state, 'done', 'all account.move.line linked to expenses must be reconciled after payment')
|
||||
|
||||
payments = self.env[action['res_model']].search(action['domain'])
|
||||
for payment in payments:
|
||||
pages = payment._check_get_pages()
|
||||
stub_line = pages[0]['stub_lines'][:1]
|
||||
self.assertTrue(stub_line)
|
||||
move = self.env[action_data['context']['active_model']].browse(action_data['context']['active_ids'])
|
||||
self.assertDictEqual(stub_line[0], {
|
||||
'due_date': payment.date.strftime("%m/%d/%Y"),
|
||||
'number': ' - '.join([move.name, move.ref] if move.ref else [move.name]),
|
||||
'amount_total': formatLang(self.env, move.amount_total, currency_obj=self.env.company.currency_id),
|
||||
'amount_residual': '-',
|
||||
'amount_paid': formatLang(self.env, payment.amount_total, currency_obj=self.env.company.currency_id),
|
||||
'currency': self.env.company.currency_id
|
||||
})
|
||||
payment = self.env[action['res_model']].browse(action['res_id'])
|
||||
pages = payment._check_get_pages()
|
||||
stub_line = pages[0]['stub_lines'][:1]
|
||||
self.assertTrue(stub_line)
|
||||
move = self.env[action_data['context']['active_model']].browse(action_data['context']['active_ids'])
|
||||
self.assertDictEqual(stub_line[0], {
|
||||
'due_date': payment.date.strftime("%m/%d/%Y"),
|
||||
'number': ' - '.join([move.name, move.ref] if move.ref else [move.name]),
|
||||
'amount_total': formatLang(self.env, move.amount_total, currency_obj=self.env.company.currency_id),
|
||||
'amount_residual': '-',
|
||||
'amount_paid': formatLang(self.env, payment.amount_total, currency_obj=self.env.company.currency_id),
|
||||
'currency': self.env.company.currency_id
|
||||
})
|
||||
|
||||
def test_hr_expense_split(self):
|
||||
"""
|
||||
|
||||
@@ -59,12 +59,12 @@ class TestExpensesAccessRights(TestExpenseCommon):
|
||||
# The expense employee is not able to approve itself the expense sheet.
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet.with_user(self.expense_user_employee).approve_expense_sheets()
|
||||
expense_sheet.with_user(self.expense_user_employee).action_approve_expense_sheets()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
|
||||
|
||||
# An expense manager is required for this step.
|
||||
|
||||
expense_sheet.with_user(self.expense_user_manager).approve_expense_sheets()
|
||||
expense_sheet.with_user(self.expense_user_manager).action_approve_expense_sheets()
|
||||
self.assertRecordValues(expense_sheet, [{'state': 'approve'}])
|
||||
|
||||
# An expense manager is not able to create the journal entry.
|
||||
@@ -108,10 +108,11 @@ class TestExpensesAccessRights(TestExpenseCommon):
|
||||
# The expense employee is not able to refuse itself the expense sheet.
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
expense_sheet.with_user(self.expense_user_employee).refuse_sheet('')
|
||||
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).refuse_sheet('')
|
||||
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'}])
|
||||
|
||||
@@ -12,6 +12,8 @@ class TestExpenseMultiCompany(TestExpenseCommon):
|
||||
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)\
|
||||
@@ -44,14 +46,14 @@ class TestExpenseMultiCompany(TestExpenseCommon):
|
||||
expense_sheet\
|
||||
.with_user(self.expense_user_manager)\
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids)\
|
||||
.approve_expense_sheets()
|
||||
.action_approve_expense_sheets()
|
||||
|
||||
# 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)\
|
||||
.approve_expense_sheets()
|
||||
.action_approve_expense_sheets()
|
||||
|
||||
# An expense manager having accounting access rights is not able to create the journal entry without access
|
||||
# to company_2.
|
||||
@@ -106,11 +108,11 @@ class TestExpenseMultiCompany(TestExpenseCommon):
|
||||
expense_sheet\
|
||||
.with_user(self.expense_user_manager)\
|
||||
.with_context(allowed_company_ids=self.company_data['company'].ids)\
|
||||
.refuse_sheet('')
|
||||
._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)\
|
||||
.refuse_sheet('')
|
||||
._do_refuse('failed')
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_payment_form_inherit_expense" model="ir.ui.view">
|
||||
<field name="name">account.payment.form.inherit</field>
|
||||
<field name="model">account.payment</field>
|
||||
<field name="inherit_id" ref="account.view_account_payment_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="expense_sheet_id" invisible="1"/>
|
||||
<button name="action_open_expense_report"
|
||||
class="oe_stat_button"
|
||||
string="Expense Report"
|
||||
icon="fa-file-text-o"
|
||||
type="object"
|
||||
attrs="{'invisible': [('expense_sheet_id', '=', [])]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -50,7 +50,7 @@
|
||||
</header>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="attachment_number" invisible="True"/>
|
||||
<field name="attachment_number" invisible="1"/>
|
||||
<field name="date" optional="show"/>
|
||||
<field name="product_id" optional="hide"/>
|
||||
<field name="name"/>
|
||||
@@ -531,8 +531,8 @@
|
||||
</group>
|
||||
<group string="Accounting">
|
||||
<field name="property_account_expense_id" groups="account.group_account_readonly"/>
|
||||
<field name="supplier_taxes_id" widget="many2many_tags"
|
||||
context="{'default_type_tax_use':'purchase', 'default_price_include': 1}"
|
||||
<field name="supplier_taxes_id" widget="many2many_tags"
|
||||
context="{'default_type_tax_use':'purchase', 'default_price_include': 1}"
|
||||
options="{'no_quick_create': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
@@ -632,6 +632,7 @@
|
||||
<field name="journal_id" optional="hide"/>
|
||||
<field name="state" optional="show" decoration-info="state == 'draft'" decoration-success="state in ['submit', 'approve', 'post', 'done']" 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"/>
|
||||
<field name="payment_mode" optional="hide"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -660,8 +661,13 @@
|
||||
<field name="can_approve" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<header>
|
||||
<button name="action_submit_sheet" states="draft" string="Submit to Manager" type="object" class="oe_highlight o_expense_sheet_submit" data-hotkey="l"/>
|
||||
<button name="approve_expense_sheets"
|
||||
<button name="action_submit_sheet"
|
||||
string="Submit to Manager"
|
||||
attrs="{'invisible': [('state', '!=', 'draft')]}"
|
||||
type="object"
|
||||
class="oe_highlight o_expense_sheet_submit"
|
||||
data-hotkey="l"/>
|
||||
<button name="action_approve_expense_sheets"
|
||||
string="Approve"
|
||||
type="object"
|
||||
data-hotkey="v"
|
||||
@@ -676,21 +682,30 @@
|
||||
attrs="{'invisible': [('state', '!=', 'approve')]}"
|
||||
groups="account.group_account_invoice"/>
|
||||
<button name="action_register_payment"
|
||||
string="Register Payment"
|
||||
type="object"
|
||||
data-hotkey="w"
|
||||
class="oe_highlight o_expense_sheet_pay"
|
||||
attrs="{'invisible': [('state', '!=', 'post')]}"
|
||||
context="{'dont_redirect_to_payments': True}"
|
||||
string="Register Payment"
|
||||
attrs="{'invisible': ['|', ('payment_state', 'in', ('paid', 'in_payment')), ('state', 'not in', ('post', 'done'))]}"
|
||||
groups="account.group_account_invoice"/>
|
||||
<button name="action_unpost"
|
||||
<button name="action_refuse_expense_sheets"
|
||||
string="Refuse"
|
||||
attrs="{'invisible': [('state', 'not in', ('submit', 'approve'))]}"
|
||||
type="object"
|
||||
groups="hr_expense.group_hr_expense_team_approver"
|
||||
data-hotkey="x"/>
|
||||
<button name="action_reset_approval_expense_sheets"
|
||||
string="Reset to Draft"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|', ('can_reset', '=', False), ('state', 'not in', ('submit', 'cancel', 'approve'))]}"
|
||||
data-hotkey="q"/>
|
||||
<button name="action_reset_expense_sheets"
|
||||
string="Reset to Draft"
|
||||
type="object"
|
||||
data-hotkey="c"
|
||||
attrs="{'invisible': [('state', '!=', 'post')]}"
|
||||
string="Cancel"
|
||||
groups="account.group_account_readonly,account.group_account_invoice"/>
|
||||
<button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit,approve" context="{'hr_expense_refuse_model':'hr.expense.sheet'}" string="Refuse" type="action" groups="hr_expense.group_hr_expense_team_approver" data-hotkey="x" />
|
||||
<button name="reset_expense_sheets" string="Reset to Draft" type="object" attrs="{'invisible': ['|', ('can_reset', '=', False), ('state', 'not in', ['submit', 'cancel', 'approve'])]}" data-hotkey="q"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,submit,approve,post,done" force_save="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
@@ -700,11 +715,11 @@
|
||||
icon="fa-file-text-o"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|', ('state', 'not in', ['post', 'done']), ('account_move_id', '=', False)]}"
|
||||
groups="account.group_account_user,account.group_account_readonly">
|
||||
groups="account.group_account_invoice">
|
||||
Journal Entry
|
||||
</button>
|
||||
<field name="account_move_id" invisible="1"/>
|
||||
<button name="action_get_expense_view"
|
||||
<button name="action_open_expense_view"
|
||||
class="oe_stat_button"
|
||||
icon="fa-file-text-o"
|
||||
type="object"
|
||||
@@ -713,8 +728,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<field name="payment_state" invisible="True"/>
|
||||
<widget name="web_ribbon" title="Posted" bg_color="bg-success" attrs="{'invisible': ['|', ('payment_state', '!=', 'paid'), ('payment_mode', '==', 'own_account')]}"/>
|
||||
<widget name="web_ribbon" title="Paid" bg_color="bg-success" attrs="{'invisible': ['|', ('payment_state', '!=', 'paid'), ('payment_mode', '==', 'company_account')]}"/>
|
||||
<widget name="web_ribbon" title="Paid" bg_color="bg-success" attrs="{'invisible': [('payment_state', '!=', 'paid')]}"/>
|
||||
<widget name="web_ribbon" title="Partial" bg_color="bg-info" attrs="{'invisible': [('payment_state', '!=', 'partial')]}"/>
|
||||
<widget name="web_ribbon" title="In Payment" attrs="{'invisible': [('payment_state', '!=', 'in_payment')]}"/>
|
||||
<div class="oe_title">
|
||||
@@ -727,8 +741,12 @@
|
||||
<group name="employee_details">
|
||||
<field name="employee_id" context="{'default_company_id': company_id}" widget="many2one_avatar_employee"/>
|
||||
<field name="payment_mode"/>
|
||||
<field name="journal_id" groups="account.group_account_invoice,account.group_account_readonly" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}" context="{'default_company_id': company_id}"/>
|
||||
<field name="bank_journal_id" groups="account.group_account_invoice,account.group_account_readonly" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'company_account')]}" 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}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}" 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}"
|
||||
attrs="{'invisible': [('payment_mode', '!=', 'company_account')], 'required': [('payment_mode', '=', 'company_account')], 'readonly': [('state', 'in', ('done', 'post'))]}"/>
|
||||
<field name="address_id" invisible="1" context="{'default_company_id': company_id}"/>
|
||||
<field name="department_id" invisible="1" context="{'default_company_id': company_id}"/>
|
||||
</group>
|
||||
@@ -747,10 +765,10 @@
|
||||
mode="tree,kanban"
|
||||
domain="[('state', '=', 'draft'), ('employee_id', '=', employee_id), ('company_id', '=', company_id)]"
|
||||
options="{'reload_on_button': True}"
|
||||
context="{'form_view_ref' : 'hr_expense.hr_expense_view_form_without_header', 'default_company_id': company_id, 'default_employee_id': employee_id}"
|
||||
context="{'form_view_ref' : 'hr_expense.hr_expense_view_form_without_header', 'default_company_id': company_id, 'default_employee_id': employee_id}"
|
||||
attrs="{'readonly': [('is_editable', '=', False)]}"
|
||||
force_save="1">
|
||||
<tree decoration-danger="is_refused" editable="bottom" class="o_expense_line_list">
|
||||
<tree editable="bottom" class="o_expense_line_list">
|
||||
<field name="date" optional="show"/>
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
@@ -776,7 +794,6 @@
|
||||
<field name="amount_tax_company" optional="hide" readonly="True"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="total_amount_company" optional="show" groups="base.group_multi_currency" readonly="True"/>
|
||||
<field name="is_refused" invisible="True"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
@@ -1042,7 +1059,6 @@
|
||||
<field name="view_mode">graph,pivot,tree,kanban,form</field>
|
||||
<field name="search_view_id" ref="hr_expense_sheet_view_search"/>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new expense report
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
<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="Company Expense Journal" help="Default accounting journal for expenses paid by the company.">
|
||||
<field name="company_expense_journal_id"/>
|
||||
</block>
|
||||
<block title="Payment">
|
||||
<setting company_dependent="1" string="Company payment methods available" 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>
|
||||
|
||||
@@ -29,6 +29,8 @@ class HrExpenseApproveDuplicate(models.TransientModel):
|
||||
|
||||
def action_approve(self):
|
||||
self.sheet_ids._do_approve()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def action_refuse(self):
|
||||
self.sheet_ids.refuse_sheet(_('Duplicate Expense'))
|
||||
self.sheet_ids._do_refuse(_('Duplicate Expense'))
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -5,41 +5,21 @@ 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 sheet refusal """
|
||||
|
||||
_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')
|
||||
sheet_ids = fields.Many2many('hr.expense.sheet')
|
||||
|
||||
@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 'sheet_ids' in fields:
|
||||
res['sheet_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.sheet_ids._do_refuse(self.reason)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
<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="sheet_ids" invisible="1"/>
|
||||
<field name="reason" class="w-100"/>
|
||||
<footer>
|
||||
<button string='Refuse' name="expense_refuse_reason" type="object" class="oe_highlight" data-hotkey="q"/>
|
||||
<button string='Refuse' name="action_refuse" type="object" class="oe_highlight" data-hotkey="q"/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@@ -894,6 +894,11 @@ class Project(models.Model):
|
||||
def _get_profitability_sequence_per_invoice_type(self):
|
||||
return {}
|
||||
|
||||
def _get_already_included_profitability_invoice_line_ids(self):
|
||||
# To be extended to avoid account.move.line overlap between
|
||||
# profitability reports.
|
||||
return []
|
||||
|
||||
def _get_user_values(self):
|
||||
return {
|
||||
'is_project_user': self.user_has_groups('project.group_project_user'),
|
||||
|
||||
@@ -68,11 +68,22 @@ class Project(models.Model):
|
||||
sequence_per_invoice_type['expenses'] = 11
|
||||
return sequence_per_invoice_type
|
||||
|
||||
def _get_already_included_profitability_invoice_line_ids(self):
|
||||
# As both purchase orders and expenses (paid by employee) create vendor bills,
|
||||
# we need to make sure they are exclusive in the profitability report.
|
||||
move_line_ids = super()._get_already_included_profitability_invoice_line_ids()
|
||||
query = self.env['account.move.line']._search([
|
||||
('move_id.expense_sheet_id', '!=', False),
|
||||
('id', 'not in', move_line_ids),
|
||||
])
|
||||
query.order = None
|
||||
return move_line_ids + list(query)
|
||||
|
||||
def _get_expenses_profitability_items(self, with_action=True):
|
||||
if not self.analytic_account_id:
|
||||
return {}
|
||||
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
query = self.env['hr.expense']._search([('is_refused', '=', False), ('state', 'in', ['approved', 'done'])])
|
||||
query = self.env['hr.expense']._search([('state', 'in', ['approved', 'done'])])
|
||||
query.order = None
|
||||
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query_string, query_param = query.select('array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestProjectHrExpenseProfitabilityCommon(TestExpenseCommon):
|
||||
'No data should be found since the sheet is not approved yet.',
|
||||
)
|
||||
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
self.assertEqual(expense_sheet.state, 'approve')
|
||||
return expense_sheet
|
||||
|
||||
@@ -65,7 +65,7 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj
|
||||
},
|
||||
)
|
||||
|
||||
expense_sheet.refuse_sheet('Test cancel expense')
|
||||
expense_sheet._do_refuse('Test cancel expense')
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
self.project_profitability_items_empty,
|
||||
|
||||
@@ -134,7 +134,7 @@ class Project(models.Model):
|
||||
**pol,
|
||||
'invoice_lines': self.env['purchase.order.line'].browse(pol['id']).invoice_lines, # One2Many cannot be queried, they are not columns
|
||||
} for pol in self._cr.dictfetchall()]
|
||||
purchase_order_line_invoice_line_ids = []
|
||||
purchase_order_line_invoice_line_ids = self._get_already_included_profitability_invoice_line_ids()
|
||||
if purchase_order_line_read:
|
||||
amount_invoiced = amount_to_invoice = 0.0
|
||||
purchase_order_line_ids = []
|
||||
|
||||
@@ -13,7 +13,7 @@ class Project(models.Model):
|
||||
if not self.analytic_account_id:
|
||||
return {}
|
||||
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
query = self.env['hr.expense']._search([('is_refused', '=', False), ('state', 'in', ['approved', 'done'])])
|
||||
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.order = None
|
||||
query_string, query_param = query.select('sale_order_id', 'product_id', 'array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0, 'to_bill': 0.0},
|
||||
)
|
||||
|
||||
expense_sheet.refuse_sheet('Test Cancel Expense')
|
||||
expense_sheet._do_refuse('Test Cancel Expense')
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestReInvoice(TestExpenseCommon, TestSaleCommon):
|
||||
],
|
||||
})
|
||||
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
self.assertRecordValues(sale_order.order_line, [
|
||||
|
||||
@@ -8,7 +8,7 @@ from odoo.tests import tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
|
||||
|
||||
def test_sale_expense(self):
|
||||
""" Test the behaviour of sales orders when managing expenses """
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
# create some expense and validate it (expense at cost)
|
||||
# Submit to Manager
|
||||
sheet = self.env['hr.expense.sheet'].create({
|
||||
'name': 'Expense for John Smith',
|
||||
'name': 'Expense for John Smith 1',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data['default_journal_purchase'].id,
|
||||
})
|
||||
@@ -46,7 +46,7 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
'sale_order_id': so.id,
|
||||
})
|
||||
# Approve
|
||||
sheet.approve_expense_sheets()
|
||||
sheet.action_approve_expense_sheets()
|
||||
# Create Expense Entries
|
||||
sheet.action_sheet_move_create()
|
||||
# expense should now be in sales order
|
||||
@@ -70,7 +70,7 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
})
|
||||
# Submit to Manager
|
||||
sheet = self.env['hr.expense.sheet'].create({
|
||||
'name': 'Expense for John Smith',
|
||||
'name': 'Expense for John Smith 2',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'journal_id': self.company_data['default_journal_purchase'].id,
|
||||
})
|
||||
@@ -86,7 +86,7 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
|
||||
'sale_order_id': so.id,
|
||||
})
|
||||
# Approve
|
||||
sheet.approve_expense_sheets()
|
||||
sheet.action_approve_expense_sheets()
|
||||
# Create Expense Entries
|
||||
sheet.action_sheet_move_create()
|
||||
# expense should now be in sales order
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="model">hr.expense.sheet</field>
|
||||
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_get_expense_view" position="after">
|
||||
<button name="action_open_expense_view" position="after">
|
||||
<button name="action_open_sale_orders"
|
||||
class="oe_stat_button"
|
||||
icon="fa-money"
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestExpenseMargin(TestExpenseCommon):
|
||||
],
|
||||
})
|
||||
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
self.assertRecordValues(sale_order.order_line[1:], [
|
||||
|
||||
Reference in New Issue
Block a user