diff --git a/addons/hr_expense/__manifest__.py b/addons/hr_expense/__manifest__.py
index d67cc4ee6a2..d1f3ab98f1d 100644
--- a/addons/hr_expense/__manifest__.py
+++ b/addons/hr_expense/__manifest__.py
@@ -36,10 +36,9 @@ This module also uses analytic accounting and is compatible with the invoice on
'wizard/hr_expense_sheet_register_payment.xml',
'views/hr_expense_views.xml',
'security/ir_rule.xml',
- 'report/report_expense_sheet.xml',
- 'views/hr_dashboard.xml',
- 'views/account_journal_dashboard.xml',
- 'views/hr_expense.xml',
+ 'report/hr_expense_report.xml',
+ 'views/hr_department_views.xml',
+ 'views/assets.xml',
'views/res_config_settings_views.xml',
],
'demo': ['data/hr_expense_demo.xml'],
diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml
index 100282658a8..45dcc2738f9 100644
--- a/addons/hr_expense/data/hr_expense_data.xml
+++ b/addons/hr_expense/data/hr_expense_data.xml
@@ -62,5 +62,13 @@
+
+
+ Your Expense Report has been refused
+
+
+
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 5380365b501..f664a8dd2f1 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -17,39 +17,49 @@ class HrExpense(models.Model):
_description = "Expense"
_order = "date desc, id desc"
- name = fields.Char(string='Expense Description', readonly=True, required=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]})
- date = fields.Date(readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=fields.Date.context_today, string="Expense Date")
- employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1))
+ @api.model
+ def _default_employee_id(self):
+ return self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
+
+ @api.model
+ def _default_product_uom_id(self):
+ return self.env['product.uom'].search([], limit=1, order='id')
+
+ @api.model
+ def _default_account_id(self):
+ return self.env['ir.property'].get('property_account_expense_categ_id', 'product.category')
+
+ name = fields.Char("Expense Description", readonly=True, required=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]})
+ date = fields.Date("Expense Date", readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=fields.Date.context_today)
+ employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_employee_id)
product_id = fields.Many2one('product.product', string='Product', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, domain=[('can_be_expensed', '=', True)], required=True)
- product_uom_id = fields.Many2one('product.uom', string='Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env['product.uom'].search([], limit=1, order='id'))
- unit_amount = fields.Float(string='Unit Price', readonly=True, required=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, digits=dp.get_precision('Product Price'))
+ product_uom_id = fields.Many2one('product.uom', string='Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_product_uom_id)
+ unit_amount = fields.Float("Unit Price", readonly=True, required=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, digits=dp.get_precision('Product Price'))
quantity = fields.Float(required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, digits=dp.get_precision('Product Unit of Measure'), default=1)
tax_ids = fields.Many2many('account.tax', 'expense_tax', 'expense_id', 'tax_id', string='Taxes', states={'done': [('readonly', True)], 'post': [('readonly', True)]})
- untaxed_amount = fields.Float(string='Subtotal', store=True, compute='_compute_amount', digits=dp.get_precision('Account'))
+ untaxed_amount = fields.Float("Subtotal", store=True, compute='_compute_amount', digits=dp.get_precision('Account'))
total_amount = fields.Monetary("Total", compute='_compute_amount', store=True, currency_field='currency_id', digits=dp.get_precision('Account'))
total_amount_company = fields.Monetary("Total (Company Currency)", compute='_compute_total_amount_company', store=True, currency_field='company_currency_id', digits=dp.get_precision('Account'))
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id)
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id)
company_currency_id = fields.Many2one('res.currency', string="Report Company Currency", related='sheet_id.currency_id', store=True)
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, oldname='analytic_account')
- account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=lambda self: self.env['ir.property'].get('property_account_expense_categ_id', 'product.category'),
- help="An expense account is expected")
+ account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=_default_account_id, help="An expense account is expected")
description = fields.Text()
payment_mode = fields.Selection([
("own_account", "Employee (to reimburse)"),
("company_account", "Company")
], default='own_account', states={'done': [('readonly', True)], 'post': [('readonly', True)], 'submitted': [('readonly', True)]}, string="Payment By")
- attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments')
+ attachment_number = fields.Integer("Number of Attachments", compute='_compute_attachment_number')
state = fields.Selection([
('draft', 'To Submit'),
('reported', 'Reported'),
('done', 'Posted'),
('refused', 'Refused')
- ], compute='_compute_state', string='Status', copy=False, index=True, readonly=True, store=True,
- help="Status of the expense.")
+ ], compute='_compute_state', string='Status', copy=False, index=True, readonly=True, store=True, help="Status of the expense.")
sheet_id = fields.Many2one('hr.expense.sheet', string="Expense Report", readonly=True, copy=False)
- reference = fields.Char(string="Bill Reference")
- is_refused = fields.Boolean(string="Explicitely Refused by manager or acccountant", readonly=True, copy=False)
+ reference = fields.Char("Bill Reference")
+ is_refused = fields.Boolean("Explicitely Refused by manager or acccountant", readonly=True, copy=False)
@api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state')
def _compute_state(self):
@@ -103,8 +113,38 @@ class HrExpense(models.Model):
if self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id:
raise UserError(_('Selected Unit of Measure does not belong to the same category as the product Unit of Measure'))
+ # ----------------------------------------
+ # ORM Overrides
+ # ----------------------------------------
+
@api.multi
- def view_sheet(self):
+ def unlink(self):
+ for expense in self:
+ if expense.state in ['done']:
+ raise UserError(_('You cannot delete a posted expense.'))
+ return super(HrExpense, self).unlink()
+
+ @api.model
+ def get_empty_list_help(self, help_message):
+ if help_message:
+ use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway')
+ alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False
+ if alias_record and alias_record.alias_domain and alias_record.alias_name:
+ link = "%(email)s" % {
+ 'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
+ }
+ return '
%s
%s
%s' % (
+ _('Click to add a new expense,'),
+ _('or send receipts by email to %s.') % (link,),
+ help_message)
+ return super(HrExpense, self).get_empty_list_help(help_message)
+
+ # ----------------------------------------
+ # Actions
+ # ----------------------------------------
+
+ @api.multi
+ def action_view_sheet(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
@@ -115,7 +155,7 @@ class HrExpense(models.Model):
}
@api.multi
- def submit_expenses(self):
+ def action_submit_expenses(self):
if any(expense.state != 'draft' for expense in self):
raise UserError(_("You cannot report twice the same line!"))
if len(self.mapped('employee_id')) != 1:
@@ -132,140 +172,45 @@ class HrExpense(models.Model):
}
}
- def _prepare_move_line(self, line):
- '''
- This function prepares move line of account.move related to an expense
- '''
- partner_id = self.employee_id.address_home_id.commercial_partner_id.id
- return {
- 'date_maturity': line.get('date_maturity'),
- 'partner_id': partner_id,
- 'name': line['name'][:64],
- 'debit': line['price'] > 0 and line['price'],
- 'credit': line['price'] < 0 and - line['price'],
- 'account_id': line['account_id'],
- 'analytic_line_ids': line.get('analytic_line_ids'),
- 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency')) or - abs(line.get('amount_currency')),
- 'currency_id': line.get('currency_id'),
- 'tax_line_id': line.get('tax_line_id'),
- 'tax_ids': line.get('tax_ids'),
- 'quantity': line.get('quantity', 1.00),
- 'product_id': line.get('product_id'),
- 'product_uom_id': line.get('uom_id'),
- 'analytic_account_id': line.get('analytic_account_id'),
- 'payment_id': line.get('payment_id'),
- 'expense_id': line.get('expense_id'),
- }
-
@api.multi
- def _compute_expense_totals(self, company_currency, account_move_lines, move_date):
- '''
- internal method used for computation of total amount of an expense in the company currency and
- in the expense currency, given the account_move_lines that will be created. It also do some small
- transformations at these account_move_lines (for multi-currency purposes)
-
- :param account_move_lines: list of dict
- :rtype: tuple of 3 elements (a, b ,c)
- a: total in company currency
- b: total in hr.expense currency
- c: account_move_lines potentially modified
- '''
+ def action_get_attachment_view(self):
self.ensure_one()
- total = 0.0
- total_currency = 0.0
- for line in account_move_lines:
- line['currency_id'] = False
- line['amount_currency'] = False
- if self.currency_id != company_currency:
- line['currency_id'] = self.currency_id.id
- line['amount_currency'] = line['price']
- line['price'] = self.currency_id.with_context(date=move_date or fields.Date.context_today(self)).compute(line['price'], company_currency)
- total -= line['price']
- total_currency -= line['amount_currency'] or line['price']
- return total, total_currency, account_move_lines
+ res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
+ res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)]
+ res['context'] = {'default_res_model': 'hr.expense', 'default_res_id': self.id}
+ return res
+
+ # ----------------------------------------
+ # Business
+ # ----------------------------------------
@api.multi
- def action_move_create(self):
- '''
- main function that is called when trying to create the accounting entries related to an expense
- '''
- move_group_by_sheet = {}
+ def _get_account_move_by_sheet(self):
+ """ Return a mapping between the expense sheet of current expense and its account move
+ :returns dict where key is a sheet id, and value is an account move record
+ """
+ move_grouped_by_sheet = {}
for expense in self:
- journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
- #create the move that will contain the accounting entries
- acc_date = expense.sheet_id.accounting_date or expense.date
- if not expense.sheet_id.id in move_group_by_sheet:
+ # create the move that will contain the accounting entries
+ account_date = expense.sheet_id.accounting_date or expense.date
+ if expense.sheet_id.id not in move_grouped_by_sheet:
+ journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
move = self.env['account.move'].create({
'journal_id': journal.id,
'company_id': self.env.user.company_id.id,
- 'date': acc_date,
+ 'date': account_date,
'ref': expense.sheet_id.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': '/',
})
- move_group_by_sheet[expense.sheet_id.id] = move
+ move_grouped_by_sheet[expense.sheet_id.id] = move
else:
- move = move_group_by_sheet[expense.sheet_id.id]
- company_currency = expense.company_id.currency_id
- diff_currency_p = expense.currency_id != company_currency
- #one account.move.line per expense (+taxes..)
- move_lines = expense._move_line_get()
-
- #create one more move line, a counterline for the total on payable account
- payment_id = False
- total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date)
- if expense.payment_mode == 'company_account':
- if not expense.sheet_id.bank_journal_id.default_credit_account_id:
- raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name))
- emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id
- journal = expense.sheet_id.bank_journal_id
- #create payment
- payment_methods = (total < 0) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids
- journal_currency = journal.currency_id or journal.company_id.currency_id
- payment = self.env['account.payment'].create({
- 'payment_method_id': payment_methods and payment_methods[0].id or False,
- 'payment_type': total < 0 and 'outbound' or 'inbound',
- 'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id,
- 'partner_type': 'supplier',
- 'journal_id': journal.id,
- 'payment_date': expense.date,
- 'state': 'reconciled',
- 'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id,
- 'amount': diff_currency_p and abs(total_currency) or abs(total),
- 'name': expense.name,
- })
- payment_id = payment.id
- else:
- if not expense.employee_id.address_home_id:
- raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
- emp_account = expense.employee_id.address_home_id.property_account_payable_id.id
-
- aml_name = expense.employee_id.name + ': ' + expense.name.split('\n')[0][:64]
- move_lines.append({
- 'type': 'dest',
- 'name': aml_name,
- 'price': total,
- 'account_id': emp_account,
- 'date_maturity': acc_date,
- 'amount_currency': diff_currency_p and total_currency or False,
- 'currency_id': diff_currency_p and expense.currency_id.id or False,
- 'payment_id': payment_id,
- 'expense_id': expense.id,
- })
-
- #convert eml into an osv-valid format
- lines = [(0, 0, expense._prepare_move_line(x)) for x in move_lines]
- move.with_context(dont_create_taxes=True).write({'line_ids': lines})
- expense.sheet_id.write({'account_move_id': move.id})
- if expense.payment_mode == 'company_account':
- expense.sheet_id.paid_expense_sheets()
- for move in move_group_by_sheet.values():
- move.post()
- return True
+ move = move_grouped_by_sheet[expense.sheet_id.id]
+ return move_grouped_by_sheet
@api.multi
- def _prepare_move_line_value(self):
+ def _get_expense_account_source(self):
self.ensure_one()
if self.account_id:
account = self.account_id
@@ -277,83 +222,168 @@ class HrExpense(models.Model):
else:
account = self.env['ir.property'].with_context(force_company=self.company_id.id).get('property_account_expense_categ_id', 'product.category')
if not account:
- raise UserError(
- _('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.'))
- aml_name = self.employee_id.name + ': ' + self.name.split('\n')[0][:64]
- move_line = {
- 'type': 'src',
- 'name': aml_name,
- 'price_unit': self.unit_amount,
- 'quantity': self.quantity,
- 'price': self.total_amount,
- 'account_id': account.id,
- 'product_id': self.product_id.id,
- 'uom_id': self.product_uom_id.id,
- 'analytic_account_id': self.analytic_account_id.id,
- 'expense_id': self.id,
- }
- return move_line
+ raise UserError(_('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.'))
+ return account
@api.multi
- def _move_line_get(self):
- account_move = []
- for expense in self:
- move_line = expense._prepare_move_line_value()
- account_move.append(move_line)
+ def _get_expense_account_destination(self):
+ self.ensure_one()
+ account_dest = self.env['account.account']
+ if self.payment_mode == 'company_account':
+ if not self.sheet_id.bank_journal_id.default_credit_account_id:
+ raise UserError(_("No credit account found for the %s journal, please configure one.") % (self.sheet_id.bank_journal_id.name))
+ account_dest = self.sheet_id.bank_journal_id.default_credit_account_id.id
+ else:
+ if not self.employee_id.address_home_id:
+ raise UserError(_("No Home Address found for the employee %s, please configure one.") % (self.employee_id.name))
+ account_dest = self.employee_id.address_home_id.property_account_payable_id.id
+ return account_dest
- # Calculate tax lines and adjust base line
+ @api.multi
+ def _get_account_move_line_values(self):
+ move_line_values_by_expense = {}
+ for expense in self:
+ move_line_name = expense.employee_id.name + ': ' + expense.name.split('\n')[0][:64]
+ account_src = expense._get_expense_account_source()
+ account_dst = expense._get_expense_account_destination()
+ account_date = expense.sheet_id.accounting_date or expense.date or fields.Date.context_today(expense)
+
+ company_currency = expense.company_id.currency_id
+ different_currency = expense.currency_id != company_currency
+
+ move_line_values = []
taxes = expense.tax_ids.with_context(round=True).compute_all(expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id)
- account_move[-1]['price'] = taxes['total_excluded']
- account_move[-1]['tax_ids'] = [(6, 0, expense.tax_ids.ids)]
+ total_amount = 0.0
+ total_amount_currency = 0.0
+ partner_id = expense.employee_id.address_home_id.commercial_partner_id.id
+
+ # source move line
+ amount_currency = expense.total_amount if different_currency else False
+ move_line_src = {
+ 'name': move_line_name,
+ 'quantity': expense.quantity or 1,
+ 'debit': taxes['total_excluded'] > 0 and taxes['total_excluded'],
+ 'credit': taxes['total_excluded'] < 0 and -taxes['total_excluded'],
+ 'amount_currency': taxes['total_excluded'] > 0 and abs(amount_currency) or -abs(amount_currency),
+ 'account_id': account_src.id,
+ 'product_id': expense.product_id.id,
+ 'product_uom_id': expense.product_uom_id.id,
+ 'analytic_account_id': expense.analytic_account_id.id,
+ 'expense_id': expense.id,
+ 'partner_id': partner_id,
+ 'tax_ids': [(6, 0, expense.tax_ids.ids)],
+ 'currency_id': expense.currency_id if different_currency else False,
+ }
+ move_line_values.append(move_line_src)
+ total_amount -= move_line_src['debit']
+ total_amount_currency -= move_line_src['amount_currency'] or move_line_src['debit']
+
+ # taxes move lines
for tax in taxes['taxes']:
- account_move.append({
- 'type': 'tax',
+ price = expense.currency_id.with_context(date=account_date).compute(tax['amount'], company_currency)
+ amount_currency = price if different_currency else False
+ move_line_tax_values = {
'name': tax['name'],
- 'price_unit': tax['amount'],
'quantity': 1,
- 'price': tax['amount'],
- 'account_id': tax['account_id'] or move_line['account_id'],
+ 'debit': price > 0 and price,
+ 'credit': price < 0 and -price,
+ 'amount_currency': price > 0 and abs(amount_currency) or -abs(amount_currency),
+ 'account_id': tax['account_id'] or move_line_src['account_id'],
'tax_line_id': tax['id'],
'expense_id': expense.id,
- })
- return account_move
+ 'partner_id': partner_id,
+ 'currency_id': expense.currency_id if different_currency else False,
+ }
+ total_amount -= price
+ total_amount_currency -= move_line_tax_values['amount_currency'] or price
+ move_line_values.append(move_line_tax_values)
+
+ # destination move line
+ move_line_dst = {
+ 'name': move_line_name,
+ 'debit': total_amount > 0 and total_amount,
+ 'credit': total_amount < 0 and -total_amount,
+ 'account_id': account_dst,
+ 'date_maturity': account_date,
+ 'amount_currency': total_amount > 0 and abs(amount_currency) or -abs(amount_currency),
+ 'currency_id': expense.currency_id.id if different_currency else False,
+ 'expense_id': expense.id,
+ 'partner_id': partner_id,
+ }
+ move_line_values.append(move_line_dst)
+
+ move_line_values_by_expense[expense.id] = move_line_values
+ return move_line_values_by_expense
@api.multi
- def unlink(self):
+ def action_move_create(self):
+ '''
+ main function that is called when trying to create the accounting entries related to an expense
+ '''
+ move_group_by_sheet = self._get_account_move_by_sheet()
+
+ move_line_values_by_expense = self._get_account_move_line_values()
+
for expense in self:
- if expense.state in ['done']:
- raise UserError(_('You cannot delete a posted expense.'))
- super(HrExpense, self).unlink()
+ company_currency = expense.company_id.currency_id
+ different_currency = expense.currency_id != company_currency
+
+ # get the account move of the related sheet
+ move = move_group_by_sheet[expense.sheet_id.id]
+
+ # get move line values
+ move_line_values = move_line_values_by_expense.get(expense.id)
+ move_line_dst = move_line_values[-1]
+ total_amount = abs(move_line_dst['debit'])
+ total_amount_currency = move_line_dst['amount_currency']
+
+ # create one more move line, a counterline for the total on payable account
+ if expense.payment_mode == 'company_account':
+ if not expense.sheet_id.bank_journal_id.default_credit_account_id:
+ raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name))
+ journal = expense.sheet_id.bank_journal_id
+ # create payment
+ payment_methods = journal.outbound_payment_method_ids if total_amount < 0 else journal.inbound_payment_method_ids
+ journal_currency = journal.currency_id or journal.company_id.currency_id
+ payment = self.env['account.payment'].create({
+ 'payment_method_id': payment_methods and payment_methods[0].id or False,
+ 'payment_type': 'outbound' if total_amount < 0 else 'inbound',
+ 'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id,
+ 'partner_type': 'supplier',
+ 'journal_id': journal.id,
+ 'payment_date': expense.date,
+ 'state': 'reconciled',
+ 'currency_id': expense.currency_id.id if different_currency else journal_currency.id,
+ 'amount': abs(total_amount_currency) if different_currency else abs(total_amount),
+ 'name': expense.name,
+ })
+ move_line_dst['payment_id'] = payment.id
+
+ # link move lines to move, and move to expense sheet
+ move.with_context(dont_create_taxes=True).write({
+ 'line_ids': [(0, 0, line) for line in move_line_values]
+ })
+ expense.sheet_id.write({'account_move_id': move.id})
+
+ if expense.payment_mode == 'company_account':
+ expense.sheet_id.paid_expense_sheets()
+
+ # post the moves
+ for move in move_group_by_sheet.values():
+ move.post()
+
+ return move_group_by_sheet
@api.multi
- def action_get_attachment_view(self):
- self.ensure_one()
- res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
- res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)]
- res['context'] = {'default_res_model': 'hr.expense', 'default_res_id': self.id}
- return res
-
- @api.multi
- def refuse_expense(self,reason):
+ def refuse_expense(self, reason):
self.write({'is_refused': True})
self.sheet_id.write({'state': 'cancel'})
self.sheet_id.message_post_with_view('hr_expense.hr_expense_template_refuse_reason',
- values={'reason': reason, 'is_sheet':False, 'name':self.name})
+ values={'reason': reason, 'is_sheet': False, 'name': self.name})
- @api.model
- def get_empty_list_help(self, help_message):
- if help_message:
- use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway')
- alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False
- if alias_record and alias_record.alias_domain and alias_record.alias_name:
- link = "%(email)s" % {
- 'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
- }
- return '%s
%s
%s' % (
- _('Click to add a new expense,'),
- _('or send receipts by email to %s.') % (link,),
- help_message)
- return super(HrExpense, self).get_empty_list_help(help_message)
+ # ----------------------------------------
+ # Mail Thread
+ # ----------------------------------------
@api.model
def message_new(self, msg_dict, custom_values=None):
@@ -410,6 +440,7 @@ class HrExpense(models.Model):
})
return super(HrExpense, self).message_new(msg_dict, custom_values)
+
class HrExpenseSheet(models.Model):
_name = "hr.expense.sheet"
@@ -417,15 +448,26 @@ class HrExpenseSheet(models.Model):
_description = "Expense Report"
_order = "accounting_date desc, id desc"
- name = fields.Char(string='Expense Report Summary', required=True)
+ @api.model
+ def _default_journal_id(self):
+ journal = self.env.ref('hr_expense.hr_expense_account_journal', raise_if_not_found=False)
+ if not journal:
+ journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1)
+ return journal.id
+
+ @api.model
+ def _default_bank_journal_id(self):
+ return self.env['account.journal'].search([('type', 'in', ['cash', 'bank'])], limit=1)
+
+ name = fields.Char('Expense Report Summary', required=True)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
- state = fields.Selection([('submit', 'Submitted'),
- ('approve', 'Approved'),
- ('post', 'Posted'),
- ('done', 'Paid'),
- ('cancel', 'Refused')
- ], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='submit', required=True,
- help='Expense Report State')
+ state = fields.Selection([
+ ('submit', 'Submitted'),
+ ('approve', 'Approved'),
+ ('post', 'Posted'),
+ ('done', 'Paid'),
+ ('cancel', 'Refused')
+ ], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='submit', required=True, help='Expense Report State')
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1))
address_id = fields.Many2one('res.partner', string="Employee Home Address")
payment_mode = fields.Selection([("own_account", "Employee (to reimburse)"), ("company_account", "Company")], related='expense_line_ids.payment_mode', default='own_account', readonly=True, string="Payment By")
@@ -434,37 +476,57 @@ class HrExpenseSheet(models.Model):
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id)
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id.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)]},
- default=lambda self: self.env['ir.model.data'].xmlid_to_object('hr_expense.hr_expense_account_journal') or self.env['account.journal'].search([('type', '=', 'purchase')], limit=1),
- 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)]}, default=lambda self: self.env['account.journal'].search([('type', 'in', ['cash', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company.")
- accounting_date = fields.Date(string="Date")
+ journal_id = fields.Many2one('account.journal', string='Expense Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, 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)]}, default=_default_bank_journal_id, help="The payment method used when the expense is paid by the company.")
+ accounting_date = fields.Date("Date")
account_move_id = fields.Many2one('account.move', string='Journal Entry', ondelete='restrict', copy=False)
department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
is_multiple_currency = fields.Boolean("Handle lines with different currencies", compute='_compute_is_multiple_currency')
+ @api.depends('expense_line_ids.total_amount_company')
+ def _compute_amount(self):
+ for sheet in self:
+ sheet.total_amount = sum(sheet.expense_line_ids.mapped('total_amount_company'))
+
@api.multi
- def check_consistency(self):
- for rec in self:
- expense_lines = rec.expense_line_ids
- if not expense_lines:
- continue
- if any(expense.employee_id != rec.employee_id for expense in expense_lines):
- raise UserError(_("Expenses must belong to the same Employee."))
- if any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines):
- raise UserError(_("Expenses must have been paid by the same entity (Company or employee)"))
+ def _compute_attachment_number(self):
+ for sheet in self:
+ sheet.attachment_number = sum(sheet.expense_line_ids.mapped('attachment_number'))
+
+ @api.depends('expense_line_ids.currency_id')
+ def _compute_is_multiple_currency(self):
+ for sheet in self:
+ sheet.is_multiple_currency = len(sheet.expense_line_ids.mapped('currency_id')) > 1
+
+ @api.onchange('employee_id')
+ def _onchange_employee_id(self):
+ self.address_id = self.employee_id.address_home_id
+ self.department_id = self.employee_id.department_id
+
+ @api.multi
+ @api.constrains('expense_line_ids')
+ def _check_payment_mode(self):
+ for sheet in self:
+ expense_lines = sheet.mapped('expense_line_ids')
+ if expense_lines and any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines):
+ raise ValidationError(_("Expenses must have been paid by the same entity (Company or employee)"))
+
+ @api.constrains('expense_line_ids', 'employee_id')
+ def _check_employee(self):
+ for sheet in self:
+ employee_ids = sheet.expense_line_ids.mapped('employee_id')
+ if len(employee_ids) > 1 or (len(employee_ids) == 1 and employee_ids != sheet.employee_id):
+ raise ValidationError(_('You cannot add expense lines of another employee.'))
@api.model
def create(self, vals):
self._create_set_followers(vals)
sheet = super(HrExpenseSheet, self).create(vals)
- sheet.check_consistency()
return sheet
@api.multi
def write(self, vals):
res = super(HrExpenseSheet, self).write(vals)
- self.check_consistency()
if vals.get('employee_id'):
self._add_followers()
return res
@@ -476,9 +538,9 @@ class HrExpenseSheet(models.Model):
raise UserError(_('You cannot delete a posted or paid expense.'))
super(HrExpenseSheet, self).unlink()
- @api.multi
- def set_to_paid(self):
- self.write({'state': 'done'})
+ # --------------------------------------------
+ # Mail Thread
+ # --------------------------------------------
@api.multi
def _track_subtype(self, init_values):
@@ -522,48 +584,9 @@ class HrExpenseSheet(models.Model):
for partner in users.mapped('partner_id'):
values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {partner.id: None}, {})[0]
- @api.onchange('employee_id')
- def _onchange_employee_id(self):
- self.address_id = self.employee_id.address_home_id
- self.department_id = self.employee_id.department_id
-
- @api.depends('expense_line_ids.total_amount_company')
- def _compute_amount(self):
- for sheet in self:
- sheet.total_amount = sum(sheet.expense_line_ids.mapped('total_amount_company'))
-
- @api.depends('expense_line_ids.currency_id')
- def _compute_is_multiple_currency(self):
- for sheet in self:
- sheet.is_multiple_currency = len(sheet.expense_line_ids.mapped('currency_id')) > 1
-
- @api.one
- def _compute_attachment_number(self):
- self.attachment_number = sum(self.expense_line_ids.mapped('attachment_number'))
-
- @api.multi
- def refuse_sheet(self, reason):
- if not self.user_has_groups('hr_expense.group_hr_expense_user'):
- raise UserError(_("Only HR Officers can refuse expenses"))
- self.write({'state': 'cancel'})
- for sheet in self:
- sheet.message_post_with_view('hr_expense.hr_expense_template_refuse_reason',
- values={'reason': reason ,'is_sheet':True ,'name':self.name})
-
- @api.multi
- def approve_expense_sheets(self):
- if not self.user_has_groups('hr_expense.group_hr_expense_user'):
- raise UserError(_("Only HR Officers can approve expenses"))
- self.write({'state': 'approve', 'responsible_id': self.env.user.id})
-
- @api.multi
- def paid_expense_sheets(self):
- self.write({'state': 'done'})
-
- @api.multi
- def reset_expense_sheets(self):
- self.mapped('expense_line_ids').write({'is_refused': False})
- return self.write({'state': 'submit'})
+ # --------------------------------------------
+ # Actions
+ # --------------------------------------------
@api.multi
def action_sheet_move_create(self):
@@ -598,16 +621,33 @@ class HrExpenseSheet(models.Model):
}
return res
- @api.one
- @api.constrains('expense_line_ids', 'employee_id')
- def _check_employee(self):
- employee_ids = self.expense_line_ids.mapped('employee_id')
- if len(employee_ids) > 1 or (len(employee_ids) == 1 and employee_ids != self.employee_id):
- raise ValidationError(_('You cannot add expense lines of another employee.'))
+ # --------------------------------------------
+ # Business
+ # --------------------------------------------
- @api.one
- @api.constrains('expense_line_ids')
- def _check_payment_mode(self):
- payment_mode = set(self.expense_line_ids.mapped('payment_mode'))
- if len(payment_mode) > 1:
- raise ValidationError(_('You cannot report expenses with different payment modes.'))
\ No newline at end of file
+ @api.multi
+ def set_to_paid(self):
+ self.write({'state': 'done'})
+
+ @api.multi
+ def approve_expense_sheets(self):
+ if not self.user_has_groups('hr_expense.group_hr_expense_user'):
+ raise UserError(_("Only HR Officers can approve expenses"))
+ self.write({'state': 'approve', 'responsible_id': self.env.user.id})
+
+ @api.multi
+ def paid_expense_sheets(self):
+ self.write({'state': 'done'})
+
+ @api.multi
+ def refuse_sheet(self, reason):
+ if not self.user_has_groups('hr_expense.group_hr_expense_user'):
+ raise UserError(_("Only HR Officers can refuse expenses"))
+ self.write({'state': 'cancel'})
+ for sheet in self:
+ sheet.message_post_with_view('hr_expense.hr_expense_template_refuse_reason', values={'reason': reason, 'is_sheet': True, 'name': self.name})
+
+ @api.multi
+ def reset_expense_sheets(self):
+ self.mapped('expense_line_ids').write({'is_refused': False})
+ return self.write({'state': 'submit'})
diff --git a/addons/hr_expense/report/report_expense_sheet.xml b/addons/hr_expense/report/hr_expense_report.xml
similarity index 100%
rename from addons/hr_expense/report/report_expense_sheet.xml
rename to addons/hr_expense/report/hr_expense_report.xml
diff --git a/addons/hr_expense/tests/common.py b/addons/hr_expense/tests/common.py
index aae4b9cd192..c7b7726bd4c 100644
--- a/addons/hr_expense/tests/common.py
+++ b/addons/hr_expense/tests/common.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo.tests import common
from odoo.addons.account.tests.account_test_no_chart import TestAccountNoChartCommon
@@ -16,10 +15,6 @@ class TestExpenseCommon(TestAccountNoChartCommon):
# The user manager is only expense manager
user_group_manager = cls.env.ref('hr_expense.group_hr_expense_manager')
cls.user_manager.write({
- 'name': 'Robert Manager',
- 'login': 'rob',
- 'email': 'rob@example.com',
- 'notification_type': 'email',
'groups_id': [(6, 0, [user_group_manager.id])],
})
@@ -30,6 +25,20 @@ class TestExpenseCommon(TestAccountNoChartCommon):
'address_id': cls.user_employee.partner_id.id,
})
+ # Create tax
+ cls.tax = cls.env['account.tax'].create({
+ 'name': 'Expense 10%',
+ 'amount': 10,
+ 'amount_type': 'percent',
+ 'type_tax_use': 'purchase',
+ 'price_include': True,
+ })
+
+ # Create analytic account
+ cls.analytic_account = cls.env['account.analytic.account'].create({
+ 'name': 'Test Analytic Account for Expenses',
+ })
+
# Expense reports
cls.journal = cls.env['account.journal'].create({
'name': 'Purchase Journal - Test',
diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py
index b3cb5227230..976ba9188ae 100644
--- a/addons/hr_expense/tests/test_expenses.py
+++ b/addons/hr_expense/tests/test_expenses.py
@@ -1,89 +1,74 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo.tests.common import TransactionCase
+from odoo.addons.hr_expense.tests.common import TestExpenseCommon
-class TestCheckJournalEntry(TransactionCase):
+class TestAccountEntry(TestExpenseCommon):
"""
Check journal entries when the expense product is having tax which is tax included.
"""
def setUp(self):
- super(TestCheckJournalEntry, self).setUp()
+ super(TestAccountEntry, self).setUp()
- self.tax = self.env['account.tax'].create({
- 'name': 'Expense 10%',
- 'amount': 10,
- 'amount_type': 'percent',
- 'type_tax_use': 'purchase',
- 'price_include': True,
- })
- self.product = self.env.ref('hr_expense.air_ticket')
- self.product.write({'supplier_taxes_id': [(6, 0, [self.tax.id])]})
+ self.setUpAdditionalAccounts()
- self.employee = self.env.ref('hr.employee_mit')
-
- # Create payable account for the expense
- user_type = self.env.ref('account.data_account_type_payable')
- account_payable = self.env['account.account'].create({
- 'code': 'X1111',
- 'name': 'HR Expense - Test Payable Account',
- 'user_type_id': user_type.id,
- 'reconcile': True
- })
- self.employee.address_home_id.property_account_payable_id = account_payable.id
-
- # Create expenses account for the expense
- user_type = self.env.ref('account.data_account_type_expenses')
- account_expense = self.env['account.account'].create({
- 'code': 'X2120',
- 'name': 'HR Expense - Test Purchase Account',
- 'user_type_id': user_type.id
- })
- # Assign it to the air ticket product
- self.product.write({'property_account_expense_id': account_expense.id})
-
- # Create Sales Journal
- company = self.env.ref('base.main_company')
- self.env['account.journal'].create({
- 'name': 'Purchase Journal - Test',
- 'code': 'HRTPJ',
- 'type': 'purchase',
- 'company_id': company.id
+ self.product_expense = self.env['product.product'].create({
+ 'name': "Delivered at cost",
+ 'standard_price': 700,
+ 'list_price': 700,
+ 'type': 'consu',
+ 'supplier_taxes_id': [(6, 0, [self.tax.id])],
+ 'default_code': 'CONSU-DELI-COST',
+ 'taxes_id': False,
+ 'property_account_expense_id': self.account_expense.id,
})
- self.expense = self.env['hr.expense.sheet'].create({
+ def test_account_entry(self):
+ """ Checking accounting move entries and analytic entries when submitting expense """
+ expense = self.env['hr.expense.sheet'].create({
'name': 'Expense for John Smith',
'employee_id': self.employee.id,
})
- self.expense_line = self.env['hr.expense'].create({
+ expense_line = self.env['hr.expense'].create({
'name': 'Car Travel Expenses',
'employee_id': self.employee.id,
- 'product_id': self.product.id,
+ 'product_id': self.product_expense.id,
'unit_amount': 700.00,
'tax_ids': [(6, 0, [self.tax.id])],
- 'sheet_id': self.expense.id,
+ 'sheet_id': expense.id,
+ 'analytic_account_id': self.analytic_account.id,
})
-
- def test_journal_entry(self):
+ expense_line._onchange_product_id()
# Submitted to Manager
- self.assertEquals(self.expense.state, 'submit', 'Expense is not in Reported state')
+ self.assertEquals(expense.state, 'submit', 'Expense is not in Reported state')
# Approve
- self.expense.approve_expense_sheets()
- self.assertEquals(self.expense.state, 'approve', 'Expense is not in Approved state')
+ expense.approve_expense_sheets()
+ self.assertEquals(expense.state, 'approve', 'Expense is not in Approved state')
# Create Expense Entries
- self.expense.action_sheet_move_create()
- self.assertEquals(self.expense.state, 'post', 'Expense is not in Waiting Payment state')
- self.assertTrue(self.expense.account_move_id.id, 'Expense Journal Entry is not created')
+ expense.action_sheet_move_create()
+ self.assertEquals(expense.state, 'post', 'Expense is not in Waiting Payment state')
+ self.assertTrue(expense.account_move_id.id, 'Expense Journal Entry is not created')
# [(line.debit, line.credit, line.tax_line_id.id) for line in self.expense.expense_line_ids.account_move_id.line_ids]
# should git this result [(0.0, 700.0, False), (63.64, 0.0, 179), (636.36, 0.0, False)]
- for line in self.expense.account_move_id.line_ids:
+ for line in expense.account_move_id.line_ids:
if line.credit:
self.assertAlmostEquals(line.credit, 700.00)
+ self.assertEquals(len(line.analytic_line_ids), 0, "The credit move line should not have analytic lines")
+ self.assertFalse(line.product_id, "Product of credit move line should be false")
else:
if not line.tax_line_id == self.tax:
self.assertAlmostEquals(line.debit, 636.36)
+ self.assertEquals(len(line.analytic_line_ids), 1, "The debit move line should have 1 analytic lines")
+ self.assertEquals(line.product_id, self.product_expense, "Product of debit move line should be the one from the expense")
else:
self.assertAlmostEquals(line.debit, 63.64)
+ self.assertEquals(len(line.analytic_line_ids), 0, "The tax move line should not have analytic lines")
+ self.assertFalse(line.product_id, "Product of tax move line should be false")
+
+ self.assertEquals(self.analytic_account.line_ids, expense.account_move_id.mapped('line_ids.analytic_line_ids'))
+ self.assertEquals(len(self.analytic_account.line_ids), 1, "Analytic Account should have only one line")
+ self.assertAlmostEquals(self.analytic_account.line_ids[0].amount, -636.36, "Amount on the only AAL is wrong")
+ self.assertEquals(self.analytic_account.line_ids[0].product_id, self.product_expense, "Product of AAL should be the one from the expense")
diff --git a/addons/hr_expense/views/hr_expense.xml b/addons/hr_expense/views/assets.xml
similarity index 54%
rename from addons/hr_expense/views/hr_expense.xml
rename to addons/hr_expense/views/assets.xml
index 48517345dcd..01ca1efbc01 100644
--- a/addons/hr_expense/views/hr_expense.xml
+++ b/addons/hr_expense/views/assets.xml
@@ -6,11 +6,5 @@
-
- Your Expense Report has been refused
-
-
diff --git a/addons/hr_expense/views/hr_dashboard.xml b/addons/hr_expense/views/hr_department_views.xml
similarity index 100%
rename from addons/hr_expense/views/hr_dashboard.xml
rename to addons/hr_expense/views/hr_department_views.xml
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index 4b25a5470f6..63847cd7a6b 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -28,9 +28,9 @@