[IMP] hr_expense: Split company payments per expense

When an expense sheet move is created, there is only one
outstanding line, but it is usual to pay separately.
That makes the reconciliation step harder.

This makes sure one move is created per expense

Task-3328877

closes odoo/odoo#126805

Related: odoo/upgrade#4873
Signed-off-by: Laurent Smet (las) <las@odoo.com>
This commit is contained in:
Julien Alardot (jual)
2023-07-13 14:47:30 +02:00
parent 35ea7f5979
commit 81fe11fabb
8 changed files with 583 additions and 231 deletions
+11 -16
View File
@@ -1,17 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from markupsafe import escape
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools.misc import frozendict
class AccountMove(models.Model):
_inherit = "account.move"
expense_sheet_id = fields.One2many('hr.expense.sheet', 'account_move_id')
expense_sheet_id = fields.Many2one('hr.expense.sheet', ondelete='set null', copy=False)
def action_open_expense_report(self):
self.ensure_one()
@@ -24,16 +23,6 @@ class AccountMove(models.Model):
'res_id': self.expense_sheet_id.id
}
# Behave exactly like a receipt for everything except the display
# This enables the synchronisation of payment terms, and sets the taxes and accounts based on the product
def is_purchase_document(self, include_receipts=False):
return bool(include_receipts and self.sudo().expense_sheet_id) 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()
@@ -67,7 +56,13 @@ class AccountMove(models.Model):
}
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
own_expense_moves = self.filtered(lambda move: move.expense_sheet_id.payment_mode == 'own_account')
own_expense_moves.write({'expense_sheet_id': False, '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)
def unlink(self):
# EXTENDS account
if self.expense_sheet_id and self.expense_sheet_id.account_move_ids - self: # If not all the payments are to be deleted
raise UserError(_("You cannot delete only some entries linked to an expense report. All entries must be deleted at the same time."))
return super().unlink()
+9 -2
View File
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from markupsafe import escape
from odoo import models, _
from odoo.exceptions import UserError
class AccountPayment(models.Model):
_inherit = "account.payment"
@@ -27,7 +28,7 @@ class AccountPayment(models.Model):
# 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)
super()._synchronize_from_moves(changed_fields)
def _synchronize_to_moves(self, changed_fields):
# EXTENDS account
@@ -41,3 +42,9 @@ class AccountPayment(models.Model):
if self.move_id.expense_sheet_id:
return escape(_("Payment created for: %s")) % self.move_id.expense_sheet_id._get_html_link()
return super()._creation_message()
def unlink(self):
# EXTENDS account
if self.expense_sheet_id and self.expense_sheet_id.account_move_ids.payment_ids - self: # If not all the payments are to be deleted
raise UserError(_("You cannot delete only some payments linked to an expense report. All payments must be deleted at the same time."))
return super().unlink()
+200 -168
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
@@ -154,7 +153,7 @@ class HrExpense(models.Model):
tax_ids = expense.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == expense.company_id)
expense.product_has_tax = bool(tax_ids)
@api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state')
@api.depends('sheet_id', 'sheet_id.account_move_ids', 'sheet_id.state')
def _compute_state(self):
for expense in self:
if not expense.sheet_id:
@@ -165,7 +164,7 @@ class HrExpense(models.Model):
expense.state = 'refused'
elif expense.sheet_id.state in {'approve', 'post'}:
expense.state = 'approved'
elif not expense.sheet_id.account_move_id:
elif not expense.sheet_id.account_move_ids:
expense.state = 'submitted'
else:
expense.state = 'done'
@@ -205,12 +204,6 @@ class HrExpense(models.Model):
extra_context={'force_price_include': True},
)
def _get_taxes_results(self, price, quantity, currency):
# Deprecated
self.ensure_one()
return self.tax_ids.with_context(force_price_include=True, round=True) \
.compute_all(price, self.currency_id, quantity, self.product_id, self.employee_id.user_id.partner_id)
@api.depends('currency_rate', 'total_amount', 'tax_ids', 'product_id', 'employee_id.user_id.partner_id', 'quantity')
def _compute_total_amount_company(self):
for expense in self:
@@ -435,7 +428,7 @@ class HrExpense(models.Model):
if 'reference' in vals:
if any(not expense.is_ref_editable for expense in self):
raise UserError(_('You are not authorized to edit the reference of this expense report.'))
res = super(HrExpense, self).write(vals)
res = super().write(vals)
if 'employee_id' in vals:
# In case expense has sheet which has only one expense_line_ids,
# then changing the expense.employee_id triggers changing the sheet.employee_id too.
@@ -511,7 +504,7 @@ class HrExpense(models.Model):
dates = todo.mapped('date')
min_date = format_date(self.env, min(dates))
max_date = format_date(self.env, max(dates))
expense_name = min_date if max_date == min_date else "%s - %s" % (min_date, max_date)
expense_name = min_date if max_date == min_date else f'{min_date} - {max_date}'
vals = {
'company_id': self.company_id.id,
@@ -536,28 +529,28 @@ class HrExpense(models.Model):
def action_submit_expenses(self):
context_vals = self._get_default_expense_sheet_values()
action_values = {
'name': _('New Expense Reports'),
'type': 'ir.actions.act_window',
'res_model': 'hr.expense.sheet',
}
if len(context_vals) > 1:
sheets = self.env['hr.expense.sheet'].create(context_vals)
return {
'name': _('New Expense Reports'),
'type': 'ir.actions.act_window',
action_values.update({
'views': [[False, "list"], [False, "form"]],
'res_model': 'hr.expense.sheet',
'domain': [('id', 'in', sheets.ids)],
'context': self.env.context,
}
})
else:
context_vals_def = {}
for key in context_vals[0]:
context_vals_def['default_' + key] = context_vals[0][key]
return {
'name': _('New Expense Report'),
'type': 'ir.actions.act_window',
action_values.update({
'views': [[False, "form"]],
'res_model': 'hr.expense.sheet',
'target': 'current',
'context': context_vals_def,
}
'context': {f'default_{key}': value for key, value in context_vals[0].items()},
})
return action_values
def action_get_attachment_view(self):
self.ensure_one()
@@ -615,7 +608,86 @@ class HrExpense(models.Model):
# Business
# ----------------------------------------
def _prepare_move_line_vals(self):
def _prepare_payments_vals(self):
self.ensure_one()
journal = self.sheet_id.journal_id
payment_method_line = self.env['account.payment.method.line'].search(
[('payment_type', '=', 'outbound'),
('journal_id', '=', journal.id),
('code', '=', 'manual'),
('company_id', '=', self.company_id.id)], limit=1)
if not payment_method_line:
raise UserError(_("You need to add a manual payment method on the journal (%s)", journal.name))
if self.currency_id != self.company_currency_id:
total_amount_currency = self.total_amount
rate = total_amount_currency / self.total_amount_company if self.total_amount_company else 1.0
else:
total_amount_currency = self.total_amount_company
rate = 1
move_lines = []
# Due to rounding and conversion mismatch between vendor bills and payments, we have to force the computation into company account
amount_currency_diff = total_amount_currency
last_expense_line = None # Used for rounding in currency issues
tax_data = self.env['account.tax']._compute_taxes([self._convert_to_tax_base_line_dict(currency=self.sheet_id.currency_id)])
for base_line_data, update in tax_data['base_lines_to_update']: # Add base lines
base_line_data.update(update)
amount_currency = self.currency_id.round(base_line_data['price_subtotal'] * rate)
expense_name = self.name.split("\n")[0][:64]
last_expense_line = base_move_line = {
'name': f'{self.employee_id.name}: {expense_name}',
'account_id': base_line_data['account'].id,
'analytic_distribution': base_line_data['analytic_distribution'],
'expense_id': self.id,
'tax_ids': [Command.set(self.tax_ids.ids)],
'tax_tag_ids': base_line_data['tax_tag_ids'],
'balance': base_line_data['price_subtotal'],
'amount_currency': amount_currency,
'currency_id': self.currency_id.id,
'product_id': base_line_data['product'].id,
}
amount_currency_diff -= amount_currency
move_lines.append(base_move_line)
for tax_line_data in tax_data['tax_lines_to_add']: # Add tax lines
amount_currency = self.sheet_id.currency_id.round(tax_line_data['tax_amount'] * rate)
last_expense_line = tax_line = {
'name': self.env['account.tax'].browse(tax_line_data['tax_id']).name,
'account_id': tax_line_data['account_id'],
'analytic_distribution': tax_line_data['analytic_distribution'],
'expense_id': self.id,
'tax_tag_ids': tax_line_data['tax_tag_ids'],
'balance': tax_line_data['tax_amount'],
'amount_currency': amount_currency,
'tax_base_amount': tax_line_data['base_amount'],
'currency_id': self.currency_id.id,
'tax_repartition_line_id': tax_line_data['tax_repartition_line_id'],
}
move_lines.append(tax_line)
amount_currency_diff -= amount_currency
if not self.sheet_id.currency_id.is_zero(amount_currency_diff) and last_expense_line:
last_expense_line['amount_currency'] += amount_currency_diff
expense_name = self.name.split("\n")[0][:64]
move_lines.append({ # Add outstanding payment line
'name': f'{self.employee_id.name}: {expense_name}',
'account_id': self.sheet_id._get_expense_account_destination(),
'balance': -self.total_amount_company,
'amount_currency': self.currency_id.round(-total_amount_currency),
'currency_id': self.currency_id.id,
})
return {
**self.sheet_id._prepare_move_vals(),
'ref': self.name,
'journal_id': journal.id,
'move_type': 'entry',
'amount': total_amount_currency,
'payment_type': 'outbound',
'partner_type': 'supplier',
'payment_method_line_id': payment_method_line.id,
'currency_id': self.currency_id.id,
'line_ids': [Command.create(line) for line in move_lines],
}
def _prepare_move_lines_vals(self):
self.ensure_one()
account = self.account_id
if not account:
@@ -625,9 +697,9 @@ class HrExpense(models.Model):
account = self.product_id.product_tmpl_id._get_product_accounts()['expense']
else:
account = self.env['ir.property']._get('property_account_expense_categ_id', 'product.category')
expense_name = self.name.split('\n')[0][:64]
return {
'name': self.employee_id.name + ': ' + self.name.split('\n')[0][:64],
'name': f'{self.employee_id.name}: {expense_name}',
'account_id': account.id,
'quantity': self.quantity or 1,
'price_unit': self.unit_amount,
@@ -681,8 +753,7 @@ class HrExpense(models.Model):
if state in {'draft', 'reported'}: # Fusion the two states into only one "To Submit" state
state = 'to_submit'
currency = currency or target_currency
amount = currency._convert(
total_amount_sum, target_currency, self.env.company, fields.Date.today())
amount = currency._convert(total_amount_sum, target_currency, self.env.company, fields.Date.today())
expense_state[state]['amount'] += amount
return expense_state
@@ -736,7 +807,7 @@ class HrExpense(models.Model):
if account:
vals['account_id'] = account.id
expense = super(HrExpense, self).message_new(msg_dict, dict(custom_values or {}, **vals))
expense = super().message_new(msg_dict, dict(custom_values or {}, **vals))
self._send_expense_success_mail(msg_dict, expense)
return expense
@@ -812,7 +883,7 @@ class HrExpense(models.Model):
body=body,
email_layout_xmlid='mail.mail_notification_light',
partner_ids=expense.employee_id.user_id.partner_id.ids,
subject='Re: %s' % msg_dict.get('subject', ''),
subject=f'Re: {msg_dict.get("subject", "")}',
subtype_xmlid='mail.mt_note',
)
else:
@@ -823,7 +894,7 @@ class HrExpense(models.Model):
'email_from': self.env.user.email_formatted,
'email_to': msg_dict.get('email_from', False),
'references': msg_dict.get('message_id'),
'subject': 'Re: %s' % msg_dict.get('subject', ''),
'subject': f'Re: {msg_dict.get("subject", "")}',
}).send()
@@ -871,6 +942,10 @@ class HrExpenseSheet(models.Model):
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')
expense_number = fields.Integer(compute='_compute_expense_number', string='Number of Expenses')
nb_account_move = fields.Integer(
string='Number of Journal Entries',
compute='_compute_nb_account_move',
)
state = fields.Selection([
('draft', 'To Submit'),
('submit', 'Submitted'),
@@ -887,19 +962,18 @@ class HrExpenseSheet(models.Model):
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)
company_currency_id = fields.Many2one(comodel_name='res.currency', string="Report Company Currency", related='company_id.currency_id')
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)
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', 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)
amount_residual = fields.Monetary(
string="Amount Due", store=True,
currency_field='currency_id',
compute='_compute_from_account_move_id')
total_amount = fields.Monetary('Total', currency_field='company_currency_id', compute='_compute_amount', store=True, tracking=True)
untaxed_amount = fields.Monetary('Untaxed Amount', currency_field='company_currency_id', compute='_compute_amount', store=True)
total_amount_taxes = fields.Monetary('Taxes', currency_field='company_currency_id', compute='_compute_amount', store=True)
amount_residual = fields.Monetary(string="Amount Due", currency_field='company_currency_id',
compute='_compute_from_account_move_ids', store=True)
currency_id = fields.Many2one('res.currency', string='Currency', states={'draft': [('readonly', False)]},
compute='_compute_currency_id', store=True, readonly=True)
@@ -911,7 +985,7 @@ class HrExpenseSheet(models.Model):
string="Payment Status",
copy=False,
tracking=True,
store=True, readonly=True, compute='_compute_from_account_move_id')
store=True, readonly=True, compute='_compute_from_account_move_ids')
payment_mode = fields.Selection(
string="Paid By",
tracking=True,
@@ -930,7 +1004,7 @@ class HrExpenseSheet(models.Model):
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)
account_move_ids = fields.One2many('account.move', 'expense_sheet_id', string='Journal Entries', readonly=True)
journal_id = fields.Many2one('account.journal', compute='_compute_journal_id', string="Expense Journal", store=True)
# === Security fields === #
@@ -950,18 +1024,35 @@ 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', 'account_move_id.amount_residual')
def _compute_from_account_move_id(self):
@api.depends('account_move_ids.payment_state', 'account_move_ids.amount_residual')
def _compute_from_account_move_ids(self):
for sheet in self:
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
if sheet.payment_mode == 'company_account':
if sheet.account_move_ids:
# when the sheet is paid by the company, the state/amount of the related account_move_ids are not relevant
# unless all moves have been reversed
sheet.amount_residual = 0.
if sheet.account_move_ids - sheet.account_move_ids.filtered('reversal_move_id'):
sheet.payment_state = 'paid'
else:
sheet.payment_state = 'reversed'
else:
sheet.amount_residual = sum(sheet.account_move_ids.mapped('amount_residual'))
payment_states = set(sheet.account_move_ids.mapped('payment_state'))
if len(payment_states) <= 1: # If only 1 move or only one state
sheet.payment_state = payment_states.pop() if payment_states else 'not_paid'
elif 'partial' in payment_states or 'paid' in payment_states: # else if any are (partially) paid
sheet.payment_state = 'partial'
else:
sheet.payment_state = 'not_paid'
else:
# Only one move is created when the expenses are paid by the employee
if sheet.account_move_ids:
sheet.amount_residual = sum(sheet.account_move_ids.mapped('amount_residual'))
sheet.payment_state = sheet.account_move_ids[:1].payment_state
else:
sheet.amount_residual = 0.0
sheet.payment_state = 'not_paid'
@api.depends('selectable_payment_method_line_ids')
def _compute_payment_method_line_id(self):
@@ -980,24 +1071,25 @@ class HrExpenseSheet(models.Model):
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')
@api.depends('account_move_ids', 'payment_state', 'approval_state')
def _compute_state(self):
for sheet in self:
if sheet.payment_state != 'not_paid':
sheet.state = 'done'
elif sheet.account_move_id:
elif sheet.account_move_ids:
sheet.state = 'post'
elif sheet.approval_state:
sheet.state = sheet.approval_state
else:
sheet.state = 'draft'
@api.depends('company_id.currency_id')
@api.depends('expense_line_ids.currency_id', 'company_currency_id')
def _compute_currency_id(self):
for sheet in self:
# Deal with a display bug when there is a company currency change after creation of the expense sheet
if not sheet.currency_id or sheet.state not in {'post', 'done', 'cancel'}:
sheet.currency_id = sheet.company_id.currency_id
if not sheet.expense_line_ids or sheet.is_multiple_currency or sheet.payment_mode == 'own_account':
sheet.currency_id = sheet.company_currency_id
else:
sheet.currency_id = sheet.expense_line_ids[0].currency_id
@api.depends('expense_line_ids.currency_id')
def _compute_is_multiple_currency(self):
@@ -1041,6 +1133,11 @@ class HrExpenseSheet(models.Model):
for sheet in self:
sheet.expense_number = result.get(sheet.id, 0)
@api.depends('account_move_ids')
def _compute_nb_account_move(self):
for sheet in self:
sheet.nb_account_move = len(sheet.account_move_ids)
@api.depends('employee_id', 'employee_id.department_id')
def _compute_from_employee_id(self):
for sheet in self:
@@ -1146,14 +1243,14 @@ class HrExpenseSheet(models.Model):
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':
if 'state' in init_values and self.state == 'cancel':
return self.env.ref('hr_expense.mt_expense_refused')
elif 'state' in init_values and self.state == 'done':
if 'state' in init_values and self.state == 'done':
return self.env.ref('hr_expense.mt_expense_paid')
return super(HrExpenseSheet, self)._track_subtype(init_values)
return super()._track_subtype(init_values)
def _message_auto_subscribe_followers(self, updated_values, subtype_ids):
res = super(HrExpenseSheet, self)._message_auto_subscribe_followers(updated_values, subtype_ids)
res = super()._message_auto_subscribe_followers(updated_values, subtype_ids)
if updated_values.get('employee_id'):
employee = self.env['hr.employee'].browse(updated_values['employee_id'])
if employee.user_id:
@@ -1215,7 +1312,7 @@ class HrExpenseSheet(models.Model):
''' 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(
return self.account_move_ids.with_context(
default_partner_bank_id=self.employee_id.sudo().bank_account_id.id
).action_register_payment()
@@ -1230,16 +1327,31 @@ class HrExpenseSheet(models.Model):
'domain': [('id', 'in', self.expense_line_ids.ids)],
}
def action_open_account_move(self):
def action_open_account_moves(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' 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,
}
if self.payment_mode == 'own_account':
res_model = 'account.move'
record_ids = self.account_move_ids
else:
res_model = 'account.payment'
record_ids = self.account_move_ids.mapped('payment_id')
action = {'type': 'ir.actions.act_window', 'res_model': res_model}
if len(self.account_move_ids) == 1:
action.update({
'name': record_ids.name,
'view_mode': 'form',
'res_id': record_ids.id,
'views': [(False, 'form')],
})
else:
action.update({
'name': _("Journal entries"),
'view_mode': 'list',
'domain': [('id', 'in', record_ids.ids)],
'views': [(False, 'list'), (False, 'form')],
})
return action
# --------------------------------------------
# Business
@@ -1315,125 +1427,45 @@ class HrExpenseSheet(models.Model):
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'].with_context(**skip_context).create([sheet._prepare_payment_vals() for sheet in company_account_sheets])
moves |= payments.move_id
moves.with_context(**skip_context).action_post()
moves = self.env['account.move'].create([sheet._prepare_bills_vals() for sheet in own_account_sheets])
payments = self.env['account.payment'].with_context(**skip_context).create([expense._prepare_payments_vals() for expense in company_account_sheets.expense_line_ids])
payment_moves = payments.move_id
moves |= payment_moves
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
for sheet in self.filtered(lambda sheet: not sheet.accounting_date):
sheet.accounting_date = sheet.account_move_ids[:1].date if sheet.account_move_ids else False
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 = self.account_move_ids.filtered(lambda _move: _move.state == 'draft')
draft_moves.unlink()
moves = self.account_move_id - draft_moves
moves = self.account_move_ids - draft_moves
moves._reverse_moves(default_values_list=[{'invoice_date': fields.Date.context_today(move), 'ref': False} for move in moves], cancel=True)
def _prepare_payment_vals(self):
self.ensure_one()
payment_method_line = self.env['account.payment.method.line'].search(
[('payment_type', '=', 'outbound'),
('journal_id', '=', self.journal_id.id),
('code', '=', 'manual'),
('company_id', '=', self.company_id.id)], limit=1)
if not payment_method_line:
raise UserError(_("You need to add a manual payment method on the journal (%s)", self.journal_id.name))
if not self.expense_line_ids or self.is_multiple_currency:
currency = self.company_id.currency_id
amount = self.total_amount
else:
currency = self.expense_line_ids[0].currency_id
amount = sum(self.expense_line_ids.mapped('total_amount'))
rate = amount / self.total_amount if self.total_amount else 0.0
move_lines = []
for expense in self.expense_line_ids:
# Due to rounding and conversion mismatch between vendor bills and payments, we have to force the computation into company account
amount_currency_diff = expense.total_amount_company if currency == expense.company_currency_id else expense.total_amount
last_expense_line = None # Used for rounding in currency issues
tax_data = self.env['account.tax']._compute_taxes([expense._convert_to_tax_base_line_dict()])
for base_line_data, update in tax_data['base_lines_to_update']: # Add base lines
base_line_data.update(update)
amount_currency = currency.round(base_line_data['price_subtotal'] * rate)
expense_name = expense.name.split("\n")[0][:64]
last_expense_line = base_move_line = {
'name': f'{expense.employee_id.name}: {expense_name}',
'account_id':base_line_data['account'].id,
'product_id': base_line_data['product'].id,
'analytic_distribution': base_line_data['analytic_distribution'],
'expense_id': expense.id,
'tax_ids': [Command.set(expense.tax_ids.ids)],
'tax_tag_ids': base_line_data['tax_tag_ids'],
'balance': base_line_data['price_subtotal'],
'amount_currency': amount_currency,
'currency_id': currency.id,
}
amount_currency_diff -= amount_currency
move_lines.append(base_move_line)
for tax_line_data in tax_data['tax_lines_to_add']: # Add tax lines
amount_currency = currency.round(tax_line_data['tax_amount'] * rate)
last_expense_line = tax_line = {
'name': self.env['account.tax'].browse(tax_line_data['tax_id']).name,
'display_type': 'tax',
'account_id': tax_line_data['account_id'],
'analytic_distribution': tax_line_data['analytic_distribution'],
'expense_id': expense.id,
'tax_tag_ids': tax_line_data['tax_tag_ids'],
'balance': tax_line_data['tax_amount'],
'amount_currency': amount_currency,
'currency_id': currency.id,
'tax_repartition_line_id': tax_line_data['tax_repartition_line_id'],
}
move_lines.append(tax_line)
amount_currency_diff -= amount_currency
if not currency.is_zero(amount_currency_diff) and last_expense_line:
last_expense_line['amount_currency'] += amount_currency_diff
expense_name = self.name.split("\n")[0][:64]
move_lines.append({ # Add outstanding payment line
'name': f'{self.employee_id.name}: {expense_name}',
'display_type': 'payment_term',
'account_id': self._get_expense_account_destination(),
'balance': -self.total_amount,
'amount_currency': currency.round(-amount),
'currency_id': currency.id,
})
return {
**self._prepare_move_vals(),
'journal_id': self.journal_id.id,
'move_type': 'entry',
'amount': amount,
'payment_type': 'outbound',
'partner_type': 'supplier',
'payment_method_line_id': payment_method_line.id,
'currency_id': currency.id,
'line_ids': [Command.create(line) for line in move_lines],
}
def _prepare_bill_vals(self):
def _prepare_bills_vals(self):
self.ensure_one()
return {
**self._prepare_move_vals(),
# 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.
'invoice_date': self.accounting_date or fields.Date.context_today(self),
'journal_id': self.journal_id.id,
'ref': self.name,
'move_type': 'in_invoice',
'partner_id': self.employee_id.sudo().work_contact_id.id,
'currency_id': self.currency_id.id,
'line_ids':[Command.create(expense._prepare_move_line_vals()) for expense in self.expense_line_ids],
'line_ids':[Command.create(expense._prepare_move_lines_vals()) for expense in self.expense_line_ids],
}
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)],
'expense_sheet_id': self.id,
}
def _validate_analytic_distribution(self):
@@ -1447,9 +1479,9 @@ class HrExpenseSheet(models.Model):
def _get_responsible_for_approval(self):
if self.user_id:
return self.user_id
elif self.employee_id.parent_id.user_id:
if 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:
if self.employee_id.department_id.manager_id.user_id:
return self.employee_id.department_id.manager_id.user_id
return self.env['res.users']
+345 -36
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged, Form
@@ -52,7 +51,7 @@ class TestExpenses(TestExpenseCommon):
''' 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}
ctx = {'active_model': 'account.move', 'active_ids': expense_sheet.account_move_ids.ids}
payment_register = self.env['account.payment.register'].with_context(**ctx).create({
'amount': amount,
'journal_id': self.company_data['default_journal_bank'].id,
@@ -81,7 +80,7 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_sheet_move_create()
self.assertEqual(expense_sheet.state, 'post', 'sheet should be posted')
move = expense_sheet.account_move_id
move = expense_sheet.account_move_ids
self.assertRecordValues(move.line_ids, [
{'debit': 304.35, 'credit': 0.0, 'reconciled': False, },
@@ -90,10 +89,10 @@ class TestExpenses(TestExpenseCommon):
])
expense_sheet.action_reset_expense_sheets()
self.assertEqual(expense_sheet.state, 'draft', 'sheet should be reverted to approved')
self.assertEqual(expense_sheet.state, 'draft', 'sheet should be reverted to draft')
reverse_move = self.env['account.move'].search([('reversed_entry_id', '=', move.id)])
self.assertFalse(expense_sheet.account_move_id)
self.assertFalse(expense_sheet.account_move_ids)
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')
@@ -109,7 +108,7 @@ class TestExpenses(TestExpenseCommon):
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()
in_payment_state = expense_sheet.account_move_ids._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')
@@ -157,19 +156,13 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_approve_expense_sheets()
expense_sheet.action_sheet_move_create()
move = expense_sheet.account_move_id
move = expense_sheet.account_move_ids
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 """
# The expense employee is able to a create an expense sheet.
@@ -214,7 +207,7 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_sheet_move_create()
# Check expense sheet journal entry values.
self.assertRecordValues(expense_sheet.account_move_id.line_ids.sorted('balance'), [
self.assertRecordValues(expense_sheet.account_move_ids.line_ids.sorted('balance'), [
# Receivable line (company currency):
{
'debit': 0.0,
@@ -273,7 +266,7 @@ class TestExpenses(TestExpenseCommon):
])
# Check expense analytic lines.
self.assertRecordValues(expense_sheet.account_move_id.line_ids.analytic_line_ids.sorted('amount'), [
self.assertRecordValues(expense_sheet.account_move_ids.line_ids.analytic_line_ids.sorted('amount'), [
{
'amount': -869.57,
'date': fields.Date.from_string('2017-01-01'),
@@ -355,11 +348,11 @@ class TestExpenses(TestExpenseCommon):
expense.action_sheet_move_create()
self.assertEqual(expense.state, 'post', 'Expense is not in Waiting Payment state')
# Should get this result [(0.0, 350.0, -700.0), (318.18, 0.0, 636.36), (31.82, 0.0, 63.64)]
analytic_line = expense.account_move_id.line_ids.analytic_line_ids
analytic_line = expense.account_move_ids.line_ids.analytic_line_ids
self.assertEqual(len(analytic_line), 1)
# Expenses paid by the employee are always translated in company currency
self.assertInvoiceValues(expense.account_move_id, [
self.assertInvoiceValues(expense.account_move_ids, [
{
'balance': 318.18, # 700 * 1:2 (rate) / 1.1 (incl. tax)
'amount_currency': 318.18,
@@ -402,11 +395,12 @@ class TestExpenses(TestExpenseCommon):
})
foreign_bank_journal = self.company_data['default_journal_bank'].copy()
foreign_bank_journal.currency_id = self.currency_data['currency'].id
foreign_bank_journal_account = foreign_bank_journal.default_account_id.copy()
foreign_bank_journal_account.currency_id = self.currency_data['currency'].id
foreign_bank_journal.default_account_id = foreign_bank_journal_account.id
foreign_bank_journal.write({
'currency_id': self.currency_data['currency'].id,
'default_account_id': foreign_bank_journal_account.id
})
expense_sheet = self.env['hr.expense.sheet'].create({
'name': "test_account_entry_multi_currency_own_account",
'employee_id': self.expense_employee.id,
@@ -418,10 +412,10 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_submit_sheet()
expense_sheet.action_approve_expense_sheets()
expense_sheet.action_sheet_move_create()
self.assertRecordValues(expense_sheet.account_move_id.payment_id, [{
self.assertRecordValues(expense_sheet.account_move_ids.payment_id, [{
'currency_id': self.currency_data['currency'].id,
}])
self.assertRecordValues(expense_sheet.account_move_id.line_ids, [
self.assertRecordValues(expense_sheet.account_move_ids.line_ids, [
{'currency_id': self.currency_data['currency'].id},
{'currency_id': self.currency_data['currency'].id},
{'currency_id': self.currency_data['currency'].id},
@@ -453,10 +447,10 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_submit_sheet()
expense_sheet.action_approve_expense_sheets()
expense_sheet.action_sheet_move_create()
self.assertRecordValues(expense_sheet.account_move_id, [{
self.assertRecordValues(expense_sheet.account_move_ids, [{
'currency_id': expense_sheet.company_id.currency_id.id,
}])
self.assertRecordValues(expense_sheet.account_move_id.line_ids, [
self.assertRecordValues(expense_sheet.account_move_ids.line_ids, [
{'currency_id': expense_sheet.company_id.currency_id.id},
{'currency_id': expense_sheet.company_id.currency_id.id},
{'currency_id': expense_sheet.company_id.currency_id.id},
@@ -554,7 +548,7 @@ class TestExpenses(TestExpenseCommon):
})],
})
sheets = expense_sheet_own_1_tax + expense_sheet_own_2_tax + expense_sheet_company_1_tax + expense_sheet_company_2_tax
self.assertRecordValues(sheets.mapped('expense_line_ids'), [
self.assertRecordValues(sheets.expense_line_ids, [
{'untaxed_amount': 869.57, 'total_amount': 1000, 'total_amount_company': 1528.90, 'amount_tax': 130.43, 'amount_tax_company': 199.42},
{'untaxed_amount': 769.23, 'total_amount': 1000, 'total_amount_company': 1528.90, 'amount_tax': 230.77, 'amount_tax_company': 352.82},
{'untaxed_amount': 869.57, 'total_amount': 1000, 'total_amount_company': 1528.90, 'amount_tax': 130.43, 'amount_tax_company': 199.42},
@@ -564,26 +558,26 @@ class TestExpenses(TestExpenseCommon):
sheets.action_submit_sheet()
sheets.action_approve_expense_sheets()
sheets.action_sheet_move_create()
self.assertRecordValues(expense_sheet_own_1_tax.account_move_id.mapped('line_ids'), [
self.assertRecordValues(expense_sheet_own_1_tax.account_move_ids.line_ids, [
{'balance': 1329.48, 'amount_currency': 1329.48, 'currency_id': self.company_data['currency'].id},
{'balance': 199.42, 'amount_currency': 199.42, 'currency_id': self.company_data['currency'].id},
{'balance': -1528.90, 'amount_currency': -1528.90, 'currency_id': self.company_data['currency'].id},
])
self.assertRecordValues(expense_sheet_own_2_tax.account_move_id.mapped('line_ids'), [
self.assertRecordValues(expense_sheet_own_2_tax.account_move_ids.line_ids, [
{'balance': 1176.08, 'amount_currency': 1176.08, 'currency_id': self.company_data['currency'].id},
{'balance': 176.41, 'amount_currency': 176.41, 'currency_id': self.company_data['currency'].id},
{'balance': 176.41, 'amount_currency': 176.41, 'currency_id': self.company_data['currency'].id}, # == 352.82 amount_tax_company
{'balance': -1528.90, 'amount_currency': -1528.90, 'currency_id': self.company_data['currency'].id},
])
self.assertRecordValues(expense_sheet_company_1_tax.account_move_id.mapped('line_ids'), [
self.assertRecordValues(expense_sheet_company_1_tax.account_move_ids.line_ids, [
{'balance': 1329.48, 'amount_currency': 869.57, 'currency_id': foreign_currency.id},
{'balance': 199.42, 'amount_currency': 130.43, 'currency_id': foreign_currency.id},
{'balance': -1528.90, 'amount_currency': -1000.00, 'currency_id': foreign_currency.id},
])
self.assertRecordValues(expense_sheet_company_2_tax.account_move_id.mapped('line_ids'), [
self.assertRecordValues(expense_sheet_company_2_tax.account_move_ids.line_ids, [
{'balance': 1176.08, 'amount_currency': 769.23, 'currency_id': foreign_currency.id},
{'balance': 176.41, 'amount_currency': 115.38, 'currency_id': foreign_currency.id}, # == 352.82 amount_tax_company & 230.77 amount_tax
{'balance': 176.41, 'amount_currency': 115.39, 'currency_id': foreign_currency.id}, # One cent more in currency due to rounding
@@ -603,8 +597,8 @@ class TestExpenses(TestExpenseCommon):
'accounting_date': '2020-01-01'
})
for i in range(2):
expense_line = self.env['hr.expense'].create({
for _i in range(2):
self.env['hr.expense'].create({
'name': 'Car Travel Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
@@ -692,7 +686,6 @@ class TestExpenses(TestExpenseCommon):
],
})
#actions
sheet.action_submit_sheet()
sheet.action_approve_expense_sheets()
@@ -776,7 +769,7 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_sheet_move_create()
# Check expense sheet journal entry values.
self.assertRecordValues(expense_sheet.account_move_id.line_ids.sorted('balance'), [
self.assertRecordValues(expense_sheet.account_move_ids.line_ids.sorted('balance'), [
# Receivable lines:
{
'balance': -345.0, # 115 + 230
@@ -836,7 +829,7 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_sheet_move_create()
# Check whether employee is set as supplier on the receipt
self.assertRecordValues(expense_sheet.account_move_id, [{
self.assertRecordValues(expense_sheet.account_move_ids, [{
'partner_id': self.expense_user_employee.partner_id.id,
}])
@@ -1052,9 +1045,9 @@ class TestExpenses(TestExpenseCommon):
expense_sheet.action_submit_sheet()
expense_sheet.action_approve_expense_sheets()
expense_sheet.action_sheet_move_create()
move = expense_sheet.account_move_id
moves = expense_sheet.account_move_ids
expected_date = fields.Date.from_string('2021-01-31')
self.assertEqual(move.invoice_date_due, expected_date, 'Bill due date should follow employee payment terms')
self.assertSequenceEqual(moves.mapped('invoice_date_due'), [expected_date], 'Bill due date should follow employee payment terms')
def test_inverse_total_amount(self):
""" Test if the inverse method works correctly """
@@ -1095,3 +1088,319 @@ class TestExpenses(TestExpenseCommon):
self.env['hr.expense'].create_expense_from_attachments(attachment.id)
expense = self.env['hr.expense'].search([], order='id desc', limit=1)
self.assertEqual(expense.account_id, product.property_account_expense_id, "The expense account should be the default one of the product")
def test_expense_sheet_payment_states(self):
sheet_company = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
Command.create({
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id
}),
Command.create({
'name': 'expense_2',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id
}),
],
})
sheet_own = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet 2',
'payment_mode': 'own_account',
'expense_line_ids': [
Command.create({
'name': 'expense_3',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 4.0,
'payment_mode': 'own_account',
'employee_id': self.expense_employee.id
}),
Command.create({
'name': 'expense_4',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 8.0,
'payment_mode': 'own_account',
'employee_id': self.expense_employee.id
}),
],
})
sheets = self.env['hr.expense.sheet'].browse((sheet_company.id, sheet_own.id))
#actions
self.assertEqual('draft', sheet_own.state)
self.assertEqual('not_paid', sheet_own.payment_state)
self.assertEqual('draft', sheet_company.state)
self.assertEqual('not_paid', sheet_company.payment_state)
sheets.action_submit_sheet()
self.assertEqual('submit', sheet_own.state)
self.assertEqual('not_paid', sheet_own.payment_state)
self.assertEqual('submit', sheet_company.state)
self.assertEqual('not_paid', sheet_company.payment_state)
sheets.action_approve_expense_sheets()
self.assertEqual('approve', sheet_own.state)
self.assertEqual('not_paid', sheet_own.payment_state)
self.assertEqual('approve', sheet_company.state)
self.assertEqual('not_paid', sheet_company.payment_state)
sheets.action_sheet_move_create()
self.assertEqual('post', sheet_own.state)
self.assertEqual('not_paid', sheet_own.payment_state)
self.assertEqual('done', sheet_company.state)
self.assertEqual('paid', sheet_company.payment_state)
own_payment = sheet_own.account_move_ids[0].action_register_payment()
own_wizard = Form(self.env['account.payment.register'].with_context(own_payment['context'])).save()
own_wizard.action_create_payments()
self.assertEqual('done', sheet_own.state)
self.assertIn(sheet_own.payment_state, {'in_payment', 'paid'})
def test_expense_sheet_payments_amount(self):
sheet_company = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
Command.create({
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id
}),
Command.create({
'name': 'expense_2',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id
}),
],
})
sheet_own = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet 2',
'payment_mode': 'own_account',
'expense_line_ids': [
Command.create({
'name': 'expense_3',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 4.0,
'payment_mode': 'own_account',
'employee_id': self.expense_employee.id
}),
Command.create({
'name': 'expense_4',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 8.0,
'payment_mode': 'own_account',
'employee_id': self.expense_employee.id
}),
],
})
sheets = self.env['hr.expense.sheet'].browse((sheet_company.id, sheet_own.id))
sheets.action_submit_sheet()
sheets.action_approve_expense_sheets()
sheets.action_sheet_move_create()
self.assertEqual(1, len(sheet_own.account_move_ids), "When an expense is paid by the employee, one move is created")
self.assertEqual(2, len(sheet_company.account_move_ids), "When an expense is paid by the company, one move is created per expense")
def test_payment_unlinks(self):
sheet_company = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
Command.create({
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id
}),
Command.create({
'name': 'expense_2',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id
}),
],
})
sheet_own = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet 2',
'payment_mode': 'own_account',
'expense_line_ids': [
Command.create({
'name': 'expense_3',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 4.0,
'payment_mode': 'own_account',
'employee_id': self.expense_employee.id
}),
Command.create({
'name': 'expense_4',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 8.0,
'payment_mode': 'own_account',
'employee_id': self.expense_employee.id
}),
],
})
sheets = sheet_company | sheet_own
sheets.action_submit_sheet()
sheets.action_approve_expense_sheets()
sheets.action_sheet_move_create()
own_payment = sheet_own.account_move_ids[0].action_register_payment()
own_wizard = Form(self.env['account.payment.register'].with_context(own_payment['context'])).save()
own_wizard.action_create_payments()
sheet_own.account_move_ids.button_draft()
sheet_own.account_move_ids.unlink()
with self.assertRaises(UserError, msg="For company-paid expenses report, deleting payments is an all-or-nothing situation"):
sheet_company.account_move_ids[:-1].payment_id.unlink()
sheet_company.account_move_ids.payment_id.unlink()
self.assertRecordValues(sheets.sorted('payment_mode'), [
{'payment_mode': 'company_account', 'state': 'approve', 'payment_state': 'not_paid', 'account_move_ids': self.env['account.move']},
{'payment_mode': 'own_account', 'state': 'approve', 'payment_state': 'not_paid', 'account_move_ids': self.env['account.move']},
])
def test_multiple_payments(self):
# pylint: disable=bad-whitespace
foreign_currency = self.env['res.currency'].create({
'name': 'Exposure',
'symbol': ' ',
'rounding': 0.01,
'position': 'after',
'currency_unit_label': 'Nothing',
'currency_subunit_label': 'Smaller Nothing',
})
self.env['res.currency.rate'].create({
'name': '2016-01-01',
'rate': 1/1.52890,
'currency_id': foreign_currency.id,
'company_id': self.company_data['company'].id,
})
tax = self.env['account.tax'].create({
'name': 'Tax Expense 15%',
'amount': 15,
'amount_type': 'percent',
'type_tax_use': 'purchase',
'price_include': True,
})
expense_sheet_own_account = self.env['hr.expense.sheet'].create({
'name': "Own Expenses",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'expense_line_ids': [
Command.create({
'name': 'Own expense',
'date': '2022-11-16',
'payment_mode': 'own_account',
'total_amount': 1000.00,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': self.company_data['currency'].id,
'tax_ids': [Command.set(tax.ids)],
}),
Command.create({
'name': 'Own expense 2',
'date': '2022-11-17',
'payment_mode': 'own_account',
'total_amount': 1000.00,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': foreign_currency.id, # rate is 1:1.5289
'tax_ids': [Command.set(tax.ids)],
})
],
})
expense_sheet_company_account = self.env['hr.expense.sheet'].create({
'name': "Company Expenses",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'expense_line_ids': [
Command.create({
'name': 'Company expense',
'date': '2022-11-16',
'payment_mode': 'company_account',
'total_amount': 1000.00,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': self.company_data['currency'].id,
'tax_ids': [Command.set(tax.ids)],
}),
Command.create({
'name': 'Company expense 2',
'date': '2022-11-17',
'payment_mode': 'company_account',
'total_amount': 1000.00,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': foreign_currency.id, # rate is 1:1.5289
'tax_ids': [Command.set(tax.ids)],
}),
],
})
sheets = expense_sheet_own_account + expense_sheet_company_account
self.assertRecordValues(sheets.expense_line_ids, [
{'total_amount': 1000.00, 'total_amount_company': 1000.00, 'amount_tax': 130.43, 'amount_tax_company': 130.43},
{'total_amount': 1000.00, 'total_amount_company': 1528.90, 'amount_tax': 130.43, 'amount_tax_company': 199.42},
{'total_amount': 1000.00, 'total_amount_company': 1000.00, 'amount_tax': 130.43, 'amount_tax_company': 130.43},
{'total_amount': 1000.00, 'total_amount_company': 1528.90, 'amount_tax': 130.43, 'amount_tax_company': 199.42},
])
sheets.action_submit_sheet()
sheets.action_approve_expense_sheets()
sheets.action_sheet_move_create()
self.assertRecordValues(expense_sheet_own_account.account_move_ids, [{'ref': 'Own Expenses'}]) # One move with the report name in ref
self.assertRecordValues(expense_sheet_company_account.account_move_ids.sorted('ref'), [
{'ref': 'Company expense'},
{'ref': 'Company expense 2'},
]) # Two moves with the expense names in ref
self.assertRecordValues(expense_sheet_own_account.account_move_ids.line_ids, [
{'balance': 1329.48, 'amount_currency': 1329.48, 'currency_id': self.company_data['currency'].id},
{'balance': 869.57, 'amount_currency': 869.57, 'currency_id': self.company_data['currency'].id},
{'balance': 199.42, 'amount_currency': 199.42, 'currency_id': self.company_data['currency'].id},
{'balance': 130.43, 'amount_currency': 130.43, 'currency_id': self.company_data['currency'].id},
{'balance': -2528.90, 'amount_currency': -2528.90, 'currency_id': self.company_data['currency'].id},
])
self.assertRecordValues(expense_sheet_company_account.account_move_ids.sorted('ref').line_ids, [
# First move
{'balance': 869.57, 'amount_currency': 869.57, 'currency_id': self.company_data['currency'].id},
{'balance': 130.43, 'amount_currency': 130.43, 'currency_id': self.company_data['currency'].id},
{'balance': -1000.00, 'amount_currency': -1000.00, 'currency_id': self.company_data['currency'].id},
# Second move
{'balance': 1329.48, 'amount_currency': 869.57, 'currency_id': foreign_currency.id},
{'balance': 199.42, 'amount_currency': 130.43, 'currency_id': foreign_currency.id},
{'balance': -1528.90, 'amount_currency': -1000.00, 'currency_id': foreign_currency.id},
])
@@ -12,7 +12,7 @@
class="oe_stat_button"
icon="fa-file-text-o"
type="object"
attrs="{'invisible': [('expense_sheet_id', '=', [])]}">
attrs="{'invisible': [('expense_sheet_id', '=', False)]}">
<div class="o_stat_info">
<span class="o_stat_text">Expense Report</span>
</div>
@@ -12,7 +12,7 @@
class="oe_stat_button"
icon="fa-file-text-o"
type="object"
attrs="{'invisible': [('expense_sheet_id', '=', [])]}">
attrs="{'invisible': [('expense_sheet_id', '=', False)]}">
<div class="o_stat_info">
<span class="o_stat_text">Expense Report</span>
</div>
+12 -5
View File
@@ -621,6 +621,7 @@
<field name="company_id" invisible="1"/>
<field name="product_ids" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="company_currency_id" invisible="1"/>
<field name="employee_id" widget="many2one_avatar_user"/>
<field name="accounting_date" optional="hide" groups="account.group_account_manager"/>
<field name="create_date" optional="hide"/>
@@ -662,10 +663,11 @@
<field name="can_reset" invisible="1"/>
<field name="can_approve" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="account_move_id" invisible="1"/>
<field name="account_move_ids" invisible="1"/>
<field name="payment_state" invisible="1"/>
<field name="is_editable" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="company_currency_id" invisible="1"/>
<header>
<button name="action_submit_sheet"
string="Submit to Manager"
@@ -693,7 +695,11 @@
data-hotkey="w"
class="oe_highlight o_expense_sheet_pay"
context="{'dont_redirect_to_payments': True}"
attrs="{'invisible': ['|', ('payment_state', 'in', ('paid', 'in_payment')), ('state', 'not in', ('post', 'done'))]}"
attrs="{'invisible': [
'|', ('payment_mode', '=', 'company_account'),
'|', ('state', 'not in', ('post', 'done')),
('payment_state', 'in', ('paid', 'in_payment')),
]}"
groups="account.group_account_invoice"/>
<button name="action_refuse_expense_sheets"
string="Refuse"
@@ -720,13 +726,14 @@
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_account_move"
<button name="action_open_account_moves"
class="oe_stat_button"
icon="fa-file-text-o"
type="object"
attrs="{'invisible': ['|', ('state', 'not in', ['post', 'done']), ('account_move_id', '=', False)]}"
attrs="{'invisible': ['|', ('state', 'not in', ['post', 'done']), ('nb_account_move', '=', 0)]}"
groups="account.group_account_invoice">
<div class="o_stat_info">
<field name="nb_account_move" class="o_stat_value"/>
<span class="o_stat_text">Journal Entry</span>
</div>
</button>
@@ -773,7 +780,7 @@
widget="many2many"
mode="tree,kanban"
domain="[
('state', '=', 'reported'),
('sheet_id', '=', False),
('employee_id', '=', employee_id),
('company_id', '=', company_id),
('payment_mode', '=?', payment_mode),
@@ -14,9 +14,11 @@ 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)])
expense_sheet = line.move_id.expense_sheet_id.filtered(lambda sheet: sheet and sheet.payment_mode == 'own_account')
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]
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]
return res
def _init_payments(self, to_process, edit_mode=False):