summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
Diffstat (limited to 'addons')
-rw-r--r--addons/hr_expense/__init__.py1
-rw-r--r--addons/hr_expense/__openerp__.py3
-rw-r--r--addons/hr_expense/data/hr_expense_data.xml13
-rw-r--r--addons/hr_expense/data/hr_expense_demo.xml16
-rw-r--r--addons/hr_expense/data/web_tip_data.xml37
-rw-r--r--addons/hr_expense/models/account_move_line.py4
-rw-r--r--addons/hr_expense/models/hr_department.py10
-rw-r--r--addons/hr_expense/models/hr_expense.py349
-rw-r--r--addons/hr_expense/report/__init__.py4
-rw-r--r--addons/hr_expense/report/hr_expense_report.py60
-rw-r--r--addons/hr_expense/report/report_expense_sheet.xml103
-rw-r--r--addons/hr_expense/security/ir.model.access.csv2
-rw-r--r--addons/hr_expense/tests/__init__.py1
-rw-r--r--addons/hr_expense/tests/test_expenses.py74
-rw-r--r--addons/hr_expense/views/hr_dashboard.xml18
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml558
-rw-r--r--addons/hr_expense/wizard/__init__.py1
-rw-r--r--addons/hr_expense/wizard/hr_expense_refuse_reason.py6
-rw-r--r--addons/hr_expense/wizard/hr_expense_register_payment.py82
-rw-r--r--addons/hr_expense/wizard/hr_expense_register_payment.xml51
-rw-r--r--addons/sale_expense/tests/test_sale_expense.py28
21 files changed, 1091 insertions, 330 deletions
diff --git a/addons/hr_expense/__init__.py b/addons/hr_expense/__init__.py
index d84fb182e8f..11529f98733 100644
--- a/addons/hr_expense/__init__.py
+++ b/addons/hr_expense/__init__.py
@@ -2,5 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
-import report
import wizard
diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py
index e82a1c3c8e0..ad04f65520b 100644
--- a/addons/hr_expense/__openerp__.py
+++ b/addons/hr_expense/__openerp__.py
@@ -32,9 +32,10 @@ This module also uses analytic accounting and is compatible with the invoice on
'data/hr_expense_data.xml',
'data/hr_expense_sequence.xml',
'wizard/hr_expense_refuse_reason.xml',
+ 'wizard/hr_expense_register_payment.xml',
'views/hr_expense_views.xml',
'security/ir_rule.xml',
- 'views/report_expense.xml',
+ 'report/report_expense_sheet.xml',
'views/hr_dashboard.xml',
'views/hr_expense.xml',
'views/tour_views.xml',
diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml
index 8af41a40832..808f1d98d9d 100644
--- a/addons/hr_expense/data/hr_expense_data.xml
+++ b/addons/hr_expense/data/hr_expense_data.xml
@@ -21,31 +21,30 @@
<!-- Expense-related subtypes for messaging / Chatter -->
<record id="mt_expense_approved" model="mail.message.subtype">
<field name="name">Approved</field>
- <field name="res_model">hr.expense</field>
- <field name="default" eval="False"/>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="default" eval="True"/>
<field name="description">Expense approved</field>
</record>
<record id="mt_expense_refused" model="mail.message.subtype">
<field name="name">Refused</field>
- <field name="res_model">hr.expense</field>
+ <field name="res_model">hr.expense.sheet</field>
<field name="default" eval="True"/>
<field name="description">Expense refused</field>
</record>
<record id="mt_expense_confirmed" model="mail.message.subtype">
<field name="name">To Approve</field>
- <field name="res_model">hr.expense</field>
+ <field name="res_model">hr.expense.sheet</field>
<field name="description">Expense confirmed, waiting confirmation</field>
- <field name="default" eval="True"/>
+ <field name="default" eval="False"/>
</record>
<!-- Department (Parent) related subtypes for messaging / Chatter -->
<record id="mt_department_expense_confirmed" model="mail.message.subtype">
<field name="name">Expenses To Approve</field>
<field name="res_model">hr.department</field>
- <field name="default" eval="True"/>
+ <field name="default" eval="False"/>
<field name="parent_id" eval="ref('mt_expense_confirmed')"/>
<field name="relation_field">department_id</field>
<field name="sequence" eval="10"/>
</record>
-
</data>
</odoo>
diff --git a/addons/hr_expense/data/hr_expense_demo.xml b/addons/hr_expense/data/hr_expense_demo.xml
index 229653b1808..bea4c5b26db 100644
--- a/addons/hr_expense/data/hr_expense_demo.xml
+++ b/addons/hr_expense/data/hr_expense_demo.xml
@@ -3,7 +3,21 @@
<data noupdate="1">
<record id="hr.employee_mit" model="hr.employee">
- <field name="address_home_id" ref="base.res_partner_address_5"/>
+ <field name="address_home_id" ref="base.res_partner_address_3"/>
+ </record>
+
+ <record id="base.res_partner_address_fp" model="res.partner">
+ <field name="name">Pieter Parter's Farm</field>
+ <field name="parent_id" eval="ref('base.partner_root')"/>
+ <field name="supplier">1</field>
+ <field name="street">Chaussée de Namur, 40</field>
+ <field name="zip">1367</field>
+ <field name="city">Grand-Rosière-Hottomont</field>
+ <field name="country_id" ref="base.be"/>
+ </record>
+
+ <record id="hr.employee_fp" model="hr.employee">
+ <field name="address_home_id" ref="base.res_partner_address_fp"/>
</record>
<record id="car_travel" model="product.product">
diff --git a/addons/hr_expense/data/web_tip_data.xml b/addons/hr_expense/data/web_tip_data.xml
new file mode 100644
index 00000000000..e45b329cd89
--- /dev/null
+++ b/addons/hr_expense/data/web_tip_data.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <record id="expense_tip_1" model="web.tip">
+ <field name="title"></field>
+ <field name="description">Select the lines you want to submit to be reimbursed.</field>
+ <field name="action_id" ref="hr_expense.hr_expense_actions_all"/>
+ <field name="model">hr.expense</field>
+ <field name="mode">list</field>
+ <field name="trigger_selector">input.oe_list_record_selector:visible</field>
+ <field name="highlight_selector">input.oe_list_record_selector:visible</field>
+ <field name="placement">bottom</field>
+ </record>
+
+ <record id="expense_tip_2" model="web.tip">
+ <field name="title"></field>
+ <field name="description">Click on Action to submit multiple expenses to your manager.</field>
+ <field name="action_id" ref="hr_expense.hr_expense_actions_all"/>
+ <field name="model">hr.expense</field>
+ <field name="mode">list</field>
+ <field name="trigger_selector">div.oe_sidebar.btn-group:visible</field>
+ <field name="highlight_selector">div.oe_sidebar.btn-group:visible</field>
+ <field name="placement">bottom</field>
+ </record>
+
+ <record id="expense_tip_3" model="web.tip">
+ <field name="title"></field>
+ <field name="description">Use the messaging tool to log a note and attach a document to the expense. (e.g. scan of the bill)</field>
+ <field name="action_id" ref="hr_expense.hr_expense_actions_all"/>
+ <field name="model">hr.expense</field>
+ <field name="mode">form</field>
+ <field name="trigger_selector">div.btn-group.btn-group-sm:visible > button</field>
+ <field name="highlight_selector">.o_timeline_compose_post.btn.btn-primary:visible</field>
+ <field name="placement">bottom</field>
+ </record>
+
+</odoo>
diff --git a/addons/hr_expense/models/account_move_line.py b/addons/hr_expense/models/account_move_line.py
index f7e24f65277..c81d8d753ef 100644
--- a/addons/hr_expense/models/account_move_line.py
+++ b/addons/hr_expense/models/account_move_line.py
@@ -12,6 +12,6 @@ class AccountMoveLine(models.Model):
res = super(AccountMoveLine, self).reconcile(writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id)
account_move_ids = [l.move_id.id for l in self if float_compare(l.move_id.matched_percentage, 1, precision_digits=5) == 0]
if account_move_ids:
- expenses = self.env['hr.expense'].search([('account_move_id', 'in', account_move_ids)])
- expenses.paid_expenses()
+ expense_sheets = self.env['hr.expense.sheet'].search([('account_move_id', 'in', account_move_ids)])
+ expense_sheets.set_to_paid()
return res
diff --git a/addons/hr_expense/models/hr_department.py b/addons/hr_expense/models/hr_department.py
index 45433dde58e..a9db1671105 100644
--- a/addons/hr_expense/models/hr_department.py
+++ b/addons/hr_expense/models/hr_department.py
@@ -7,10 +7,10 @@ from openerp import fields, models
class HrDepartment(models.Model):
_inherit = 'hr.department'
- def _compute_expense_to_approve(self):
- expense_data = self.env['hr.expense'].read_group([('department_id', 'in', self.ids), ('state', '=', 'submit')], ['department_id'], ['department_id'])
- result = dict((data['department_id'][0], data['department_id_count']) for data in expense_data)
+ def _compute_expense_sheets_to_approve(self):
+ expense_sheet_data = self.env['hr.expense.sheet'].read_group([('department_id', 'in', self.ids), ('state', '=', 'submit')], ['department_id'], ['department_id'])
+ result = dict((data['department_id'][0], data['department_id_count']) for data in expense_sheet_data)
for department in self:
- department.expense_to_approve_count = result.get(department.id, 0)
+ department.expense_sheets_to_approve_count = result.get(department.id, 0)
- expense_to_approve_count = fields.Integer(compute='_compute_expense_to_approve', string='Expenses to Approve')
+ expense_sheets_to_approve_count = fields.Integer(compute='_compute_expense_sheets_to_approve', string='Expenses Reports to Approve')
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 15fc0473d3e..f8d6ec8be3a 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -12,11 +12,10 @@ class HrExpense(models.Model):
_name = "hr.expense"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_description = "Expense"
- _order = "date desc"
+ _order = "date desc, id desc"
- name = fields.Char(string='Expense Description', readonly=True, required=True, states={'draft': [('readonly', False)]})
+ name = fields.Char(string='Expense Description', readonly=True, required=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today)
date = fields.Date(readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, string="Date")
- accounting_date = fields.Date(readonly=True, states={'draft': [('readonly', False)]}, string='Accounting Date')
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1))
product_id = fields.Many2one('product.product', string='Product', readonly=True, states={'draft': [('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)]}, default=lambda self: self.env['product.uom'].search([], limit=1, order='id'))
@@ -29,22 +28,29 @@ class HrExpense(models.Model):
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id)
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'))
- department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
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)]}, string="Payment By")
- journal_id = fields.Many2one('account.journal', string='Expense Journal', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, default=lambda self: 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', ['case', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company.")
- account_move_id = fields.Many2one('account.move', string='Journal Entry', copy=False, track_visibility="onchange")
attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments')
- state = fields.Selection([('draft', 'To Submit'),
- ('submit', 'Submitted'),
- ('approve', 'Approved'),
- ('post', 'Waiting Payment'),
- ('done', 'Paid'),
- ('cancel', 'Refused')
- ], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='draft', required=True,
- help='When the expense request is created the status is \'To Submit\'.\n It is submitted by the employee and request is sent to manager, the status is \'Submitted\'.\
- \nIf the manager approve it, the status is \'Approved\'.\n If the accountant genrate the accounting entries for the expense request, the status is \'Waiting Payment\'.')
+ state = fields.Selection([('draft', 'To Report'),
+ ('reported', 'Reported'),
+ ('done', 'Posted'),
+ ('refused', 'Refused')
+ ], compute='_compute_state', default="draft", string='Status', index=True, readonly=True, copy=False, required=True, store=True,
+ help="Status of the expense.")
+ sheet_id = fields.Many2one('hr.expense.sheet', string="Expense Report", readonly=True)
+ reference = fields.Char(string="Bill Reference")
+
+ @api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state')
+ def _compute_state(self):
+ for expense in self:
+ if not expense.sheet_id:
+ expense.state = "draft"
+ elif expense.sheet_id.state == "cancel":
+ expense.state = "refused"
+ elif not expense.sheet_id.account_move_id:
+ expense.state = "reported"
+ else:
+ expense.state = "done"
@api.depends('quantity', 'unit_amount', 'tax_ids', 'currency_id')
def _compute_amount(self):
@@ -60,6 +66,13 @@ class HrExpense(models.Model):
for expense in self:
expense.attachment_number = attachment.get(expense.id, 0)
+ @api.multi
+ def action_get_attachment_view(self):
+ res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
+ res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)]
+ res['context'] = {'default_res_model': 'hr.expense', 'default_res_id': self.id}
+ return res
+
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
@@ -77,76 +90,31 @@ 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'))
- @api.onchange('employee_id')
- def _onchange_employee_id(self):
- self.department_id = self.employee_id.department_id
-
- def _add_followers(self):
- user_ids = []
- employee = self.employee_id
- if employee.user_id:
- user_ids.append(employee.user_id.id)
- if employee.parent_id:
- user_ids.append(employee.parent_id.user_id.id)
- if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id:
- user_ids.append(employee.department_id.manager_id.user_id.id)
- self.message_subscribe_users(user_ids=user_ids)
-
- @api.model
- def create(self, vals):
- hr_expense = super(HrExpense, self).create(vals)
- if vals.get('employee_id'):
- hr_expense._add_followers()
- return hr_expense
-
- @api.multi
- def write(self, vals):
- res = super(HrExpense, self).write(vals)
- if vals.get('employee_id'):
- self._add_followers()
- return res
-
@api.multi
- def unlink(self):
- if any(expense.state not in ['draft', 'cancel'] for expense in self):
- raise UserError(_('You can only delete draft or refused expenses!'))
- return super(HrExpense, self).unlink()
+ def view_sheet(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'hr.expense.sheet',
+ 'target': 'current',
+ 'res_id': self.sheet_id.id
+ }
@api.multi
def submit_expenses(self):
if any(expense.state != 'draft' for expense in self):
- raise UserError(_("You can only submit draft expenses!"))
- self.write({'state': 'submit'})
-
- @api.multi
- def approve_expenses(self):
- self.write({'state': 'approve'})
-
- @api.multi
- def refuse_expenses(self, reason):
- self.write({'state': 'cancel'})
- if self.employee_id.user_id:
- body = (_("Your Expense %s has been refused.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (self.name, reason))
- self.message_post(body=body, partner_ids=[self.employee_id.user_id.partner_id.id])
-
- @api.multi
- def paid_expenses(self):
- self.write({'state': 'done'})
-
- @api.multi
- def reset_expenses(self):
- return self.write({'state': 'draft'})
-
- @api.multi
- def _track_subtype(self, init_values):
- self.ensure_one()
- if 'state' in init_values and self.state == 'approve':
- return 'hr_expense.mt_expense_approved'
- elif 'state' in init_values and self.state == 'submit':
- return 'hr_expense.mt_expense_confirmed'
- elif 'state' in init_values and self.state == 'cancel':
- return 'hr_expense.mt_expense_refused'
- return super(HrExpense, self)._track_subtype(init_values)
+ raise UserError(_("You cannot report twice the same line!"))
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'hr.expense.sheet',
+ 'target': 'current',
+ 'context': {
+ 'default_expense_line_ids': [line.id for line in self],
+ 'default_employee_id': self[0].employee_id.id,
+ }
+ }
def _prepare_move_line(self, line):
'''
@@ -157,15 +125,16 @@ class HrExpense(models.Model):
'date_maturity': line.get('date_maturity'),
'partner_id': partner_id,
'name': line['name'][:64],
+ 'date': self.sheet_id.accounting_date,
'debit': line['price'] > 0 and line['price'],
- 'credit': 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')),
+ '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'),
'ref': line.get('ref'),
- 'quantity': line.get('quantity',1.00),
+ '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'),
@@ -203,22 +172,10 @@ class HrExpense(models.Model):
'''
main function that is called when trying to create the accounting entries related to an expense
'''
- if any(expense.state != 'approve' for expense in self):
- raise UserError(_("You can only generate accounting entry for approved expense(s)."))
-
- if any(expense.employee_id != self[0].employee_id for expense in self):
- raise UserError(_("Expenses must belong to the same Employee."))
-
- if any(not expense.journal_id for expense in self):
- raise UserError(_("Expenses must have an expense journal specified to generate accounting entries."))
-
journal_dict = {}
- maxdate = False
for expense in self:
- acc_date = expense.accounting_date or expense.date
- if acc_date > maxdate:
- maxdate = acc_date
- jrn = expense.bank_journal_id if expense.payment_mode == 'company_account' else expense.journal_id
+ acc_date = expense.sheet_id.accounting_date or fields.Date.context_today(self)
+ jrn = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
journal_dict.setdefault(jrn, [])
journal_dict[jrn].append(expense)
@@ -227,7 +184,7 @@ class HrExpense(models.Model):
move = self.env['account.move'].create({
'journal_id': journal.id,
'company_id': self.env.user.company_id.id,
- 'date': maxdate,
+ 'date': acc_date,
})
for expense in expense_list:
company_currency = expense.company_id.currency_id
@@ -236,11 +193,11 @@ class HrExpense(models.Model):
move_lines = expense._move_line_get()
#create one more move line, a counterline for the total on payable account
- total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, maxdate)
+ total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date)
if expense.payment_mode == 'company_account':
- if not expense.bank_journal_id.default_credit_account_id:
- raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name))
- emp_account = expense.bank_journal_id.default_credit_account_id.id
+ 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
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))
@@ -251,18 +208,18 @@ class HrExpense(models.Model):
'name': expense.employee_id.name,
'price': total,
'account_id': emp_account,
- 'date_maturity': expense.accounting_date or expense.date,
+ '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,
'ref': expense.employee_id.address_home_id.ref or False
})
#convert eml into an osv-valid format
- lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines)
+ lines = map(lambda x: (0, 0, expense._prepare_move_line(x)), move_lines)
move.write({'line_ids': lines})
- expense.write({'account_move_id': move.id, 'state': 'post'})
+ expense.sheet_id.write({'account_move_id': move.id})
if expense.payment_mode == 'company_account':
- expense.paid_expenses()
+ expense.sheet_id.paid_expense_sheets()
move.post()
return True
@@ -270,8 +227,8 @@ class HrExpense(models.Model):
def _move_line_get(self):
account_move = []
for expense in self:
- if expense.account_id:
- account = expense.account_id
+ if expense.sheet_id and expense.sheet_id.account_id:
+ account = expense.sheet_id.account_id
elif expense.product_id:
account = expense.product_id.product_tmpl_id._get_product_accounts()['expense']
if not account:
@@ -291,7 +248,7 @@ class HrExpense(models.Model):
'product_id': expense.product_id.id,
'uom_id': expense.product_uom_id.id,
'analytic_account_id': expense.analytic_account_id.id,
- }
+ }
account_move.append(move_line)
# Calculate tax lines and adjust base line
@@ -317,3 +274,175 @@ class HrExpense(models.Model):
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
+
+class HrExpenseSheet(models.Model):
+
+ _name = "hr.expense.sheet"
+ _inherit = ['mail.thread', 'ir.needaction_mixin']
+ _description = "Expense Report"
+ _order = "accounting_date desc"
+
+ name = fields.Char(string='Expense Report Summary', required=True)
+ expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'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')
+ 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")
+ responsible_id = fields.Many2one('res.users', 'Validation By', readonly=True, copy=False, states={'submit': [('readonly', False)], 'submit': [('readonly', False)]})
+ total_amount = fields.Float(string='Total Amount', store=True, compute='_compute_amount', digits=dp.get_precision('Account'))
+ 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', ['case', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company.")
+ accounting_date = fields.Date(string="Accounting Date")
+ 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_payable_id', 'account.chart.template'))
+ account_move_id = fields.Many2one('account.move', string='Journal Entry', copy=False, track_visibility="onchange")
+ department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
+
+ @api.multi
+ def check_consistency(self):
+ if any(sheet.employee_id != self[0].employee_id for sheet in self):
+ raise UserError(_("Expenses must belong to the same Employee."))
+
+ expense_lines = self.mapped('expense_line_ids')
+ if expense_lines and 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)"))
+
+ @api.model
+ def create(self, vals):
+ sheet = super(HrExpenseSheet, self).create(vals)
+ self.check_consistency()
+ if vals.get('employee_id'):
+ sheet._add_followers()
+ 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
+
+ @api.multi
+ def set_to_paid(self):
+ self.write({'state': 'done'})
+
+ @api.multi
+ def _track_subtype(self, init_values):
+ self.ensure_one()
+ if 'state' in init_values and self.state == 'approve':
+ return 'hr_expense.mt_expense_approved'
+ elif 'state' in init_values and self.state == 'submit':
+ return 'hr_expense.mt_expense_confirmed'
+ elif 'state' in init_values and self.state == 'cancel':
+ return 'hr_expense.mt_expense_refused'
+ return super(HrExpenseSheet, self)._track_subtype(init_values)
+
+ def _add_followers(self):
+ user_ids = []
+ employee = self.employee_id
+ if employee.user_id:
+ user_ids.append(employee.user_id.id)
+ if employee.parent_id:
+ user_ids.append(employee.parent_id.user_id.id)
+ if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id:
+ user_ids.append(employee.department_id.manager_id.user_id.id)
+ self.message_subscribe_users(user_ids=user_ids)
+
+ @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
+ self.account_id = self.employee_id.address_home_id.id and self.employee_id.address_home_id.property_account_payable_id.id
+
+ @api.onchange('payment_mode')
+ def _onchange_payment_mode(self):
+ if self.payment_mode == 'own_account':
+ self.account_id = self.employee_id.address_home_id.property_account_payable_id.id
+ else:
+ self.account_id = self.bank_journal_id.default_credit_account_id.id
+
+ @api.one
+ @api.depends('expense_line_ids')
+ def _compute_amount(self):
+ self.total_amount = sum(self.expense_line_ids.mapped('total_amount'))
+
+ # FIXME: A 4 command is missing to explicitly declare the one2many relation
+ # between the sheet and the lines when using 'default_expense_line_ids':[ids]
+ # in the context. A fix from chm-odoo should come since
+ # several saas versions but sadly I had to add this hack to avoid this
+ # issue
+ @api.model
+ def _add_missing_default_values(self, values):
+ values = super(HrExpenseSheet, self)._add_missing_default_values(values)
+ if self.env.context.get('default_expense_line_ids', False):
+ lines_to_add = []
+ for line in values.get('expense_line_ids', []):
+ if line[0] == 1:
+ lines_to_add.append([4, line[1], False])
+ values['expense_line_ids'] = lines_to_add + values['expense_line_ids']
+ return values
+
+ @api.one
+ def _compute_attachment_number(self):
+ self.attachment_number = sum(self.expense_line_ids.mapped('attachment_number'))
+
+ @api.multi
+ def refuse_expenses(self, reason):
+ self.write({'state': 'cancel'})
+ for sheet in self:
+ body = (_("Your Expense %s has been refused.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (sheet.name, reason))
+ sheet.message_post(body=body)
+
+ @api.multi
+ def approve_expense_sheets(self):
+ self.message_post(body=_("The expense has been validated by %s") % (self.env.user.name))
+ self.write({'state': 'approve', 'responsible_id': self.env.user.id})
+
+ @api.multi
+ def refuse_expense_sheets(self, reason):
+ self.write({'state': 'cancel'})
+ for sheet in self:
+ body = (_("Your Expense %s has been refused.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (sheet.name, reason))
+ sheet.message_post(body=body)
+
+ @api.multi
+ def paid_expense_sheets(self):
+ self.write({'state': 'done'})
+
+ @api.multi
+ def reset_expense_sheets(self):
+ return self.write({'state': 'submit'})
+
+ @api.multi
+ def action_sheet_move_create(self):
+ if any(sheet.state != 'approve' for sheet in self):
+ raise UserError(_("You can only generate accounting entry for approved expense(s)."))
+
+ if any(not sheet.journal_id for sheet in self):
+ raise UserError(_("Expenses must have an expense journal specified to generate accounting entries."))
+
+ res = self.mapped('expense_line_ids').action_move_create()
+
+ if not self.accounting_date:
+ self.accounting_date = self.account_move_id.date
+
+ self.write({'state': 'post'})
+ return res
+
+ @api.multi
+ def action_get_attachment_view(self):
+ res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
+ res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.expense_line_ids.ids)]
+ res['context'] = {'default_res_model': 'hr.expense.sheet', 'default_res_id': self.id}
+ return res
diff --git a/addons/hr_expense/report/__init__.py b/addons/hr_expense/report/__init__.py
deleted file mode 100644
index 7f74fc905a4..00000000000
--- a/addons/hr_expense/report/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import hr_expense_report \ No newline at end of file
diff --git a/addons/hr_expense/report/hr_expense_report.py b/addons/hr_expense/report/hr_expense_report.py
deleted file mode 100644
index 1422abf9aa5..00000000000
--- a/addons/hr_expense/report/hr_expense_report.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import time
-from openerp.osv import osv
-from openerp.tools.misc import formatLang
-from openerp.tools.translate import _
-from openerp.report import report_sxw
-from openerp.exceptions import UserError
-
-
-class report_expense(report_sxw.rml_parse):
-
- def set_context(self, objects, data, ids, report_type=None):
- res = super(report_expense, self).set_context(objects, data, ids, report_type=report_type)
-
- hr_expense_obj = self.pool.get('hr.expense')
-
- context = {'lang': self.pool.get('res.users').browse(self.cr, self.uid, self.uid).lang}
- state_field = hr_expense_obj.fields_get(self.cr, self.uid, 'state', context=context)['state']['selection']
- state_dict = {}
- for state_tuple in state_field:
- state_dict[state_tuple[0]] = state_tuple[1]
-
- expenses_info = {}
- expenses = hr_expense_obj.search(self.cr, self.uid, [('id', 'in', ids)], order="employee_id, currency_id, state, date")
- for expense in hr_expense_obj.browse(self.cr, self.uid, expenses):
- key = expense.employee_id.name + '-' + expense.currency_id.name + '-' + expense.state
- if expenses_info.get(key):
- expenses_info[key]['lines'] += expense
- expenses_info[key]['total_amount'] += expense.total_amount
- else:
- expenses_info[key] = {
- 'employee_name': expense.employee_id.name,
- 'total_amount': expense.total_amount,
- 'lines': expense,
- 'currency': expense.currency_id,
- 'validator_name': expense.employee_id.parent_id.name,
- 'notes': [],
- 'notes_index': {},
- 'state': state_dict[expense.state],
- }
- if expense.description:
- index = len(expenses_info[key]['notes']) + 1
- expenses_info[key]['notes'].append({'description': expense.description, 'index':index})
- expenses_info[key]['notes_index'][expense.id] = index
-
- # Qweb for-each do not work on dict, so we send a list and we sort it by the name of the employee
- # that way if we have two sheet for the same employee they will follow in the report
- self.localcontext.update({
- 'get_expenses': lambda : [v for k,v in sorted(expenses_info.items())],
- })
- return res
-
-
-class report_hr_expense(osv.AbstractModel):
- _name = 'report.hr_expense.report_expense'
- _inherit = 'report.abstract_report'
- _template = 'hr_expense.report_expense'
- _wrapped_report_class = report_expense
diff --git a/addons/hr_expense/report/report_expense_sheet.xml b/addons/hr_expense/report/report_expense_sheet.xml
new file mode 100644
index 00000000000..52d26268668
--- /dev/null
+++ b/addons/hr_expense/report/report_expense_sheet.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="report_expense_sheet">
+ <t t-call="report.html_container">
+ <t t-foreach="docs" t-as="o">
+ <t t-call="report.external_layout">
+ <div class="page">
+ <h2>Expenses Report</h2>
+
+ <div class="row mt32 mb32">
+ <div class="col-xs-2">
+ <strong>Employee:</strong>
+ <p t-field="o.employee_id.name"/>
+ </div>
+ <div class="col-xs-2">
+ <strong>Date:</strong>
+ <p t-field="o.accounting_date"/>
+ </div>
+ <div class="col-xs-3">
+ <strong>Description:</strong>
+ <p t-field="o.name"/>
+ </div>
+ <div class="col-xs-2">
+ <strong>Validated By:</strong>
+ <p t-field="o.responsible_id"/>
+ </div>
+ <div class="col-xs-3">
+ <strong>Payment By:</strong>
+ <p t-field="o.payment_mode"/>
+ </div>
+ </div>
+
+ <table class="table table-condensed">
+ <thead>
+ <tr>
+ <th>Date</th>
+ <th>Name</th>
+ <th class="text-center">Ref.</th>
+ <th>Unit Price</th>
+ <th>Taxes</th>
+ <th class="text-center">Qty</th>
+ <th class="text-right">Price</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="o.expense_line_ids" t-as="line">
+ <td><span t-field="line.date"/></td>
+ <td>
+ <span t-field="line.name"/>
+ <span t-field="line.description"/><br/>
+ <span t-field="line.analytic_account_id.name"/>
+ </td>
+ <td style="text-center">
+ <span t-field="line.reference"/>
+ </td>
+ <td>
+ <span t-field="line.unit_amount"/>
+ </td>
+ <td>
+ <span t-field="line.tax_ids"/>
+ </td>
+ <td class="text-center">
+ <span t-field="line.quantity"/>
+ </td>
+ <td class="text-right">
+ <span t-field="line.total_amount"
+ t-field-options='{"widget": "monetary", "display_currency":"o.currency_id"}'/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="row">
+ <div class="col-xs-4 pull-right">
+ <table class="table table-condensed">
+ <tr class="border-black">
+ <td><strong>Total</strong></td>
+ <td class="text-right">
+ <span t-field="o.total_amount"
+ t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <p>Certified honest and conform,<br/>(Date and signature).<br/><br/></p>
+ </div>
+ </t>
+ </t>
+ </t>
+ </template>
+
+ <report
+ id="action_report_hr_expense_sheet"
+ string="Expenses Report"
+ model="hr.expense.sheet"
+ report_type="qweb-pdf"
+ name="hr_expense.report_expense_sheet"
+ file="hr_expense.report_expense_sheet"
+ />
+
+</odoo> \ No newline at end of file
diff --git a/addons/hr_expense/security/ir.model.access.csv b/addons/hr_expense/security/ir.model.access.csv
index c1d8735ddcf..0e60e20c017 100644
--- a/addons/hr_expense/security/ir.model.access.csv
+++ b/addons/hr_expense/security/ir.model.access.csv
@@ -2,6 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_expense_user,hr.expense.user,model_hr_expense,base.group_hr_user,1,1,1,1
access_hr_expense_employee,hr.expense.employee,model_hr_expense,base.group_user,1,1,1,1
+access_hr_expense_sheet_user,hr.expense.sheet.user,model_hr_expense_sheet,base.group_hr_user,1,1,1,1
+access_hr_expense_sheet_employee,hr.expense.sheet.employee,model_hr_expense_sheet,base.group_user,1,1,1,1
access_product_product_hr_expense_user,product.product.hr.expense.user,product.model_product_product,base.group_hr_user,1,1,1,1
access_product_template_hr_expense_user,product.template.hr.expense.user,product.model_product_template,base.group_hr_user,1,1,1,1
access_product_uom_hr_expense_user,product.uom.hr.expense.user,product.model_product_uom,base.group_hr_user,1,1,1,1
diff --git a/addons/hr_expense/tests/__init__.py b/addons/hr_expense/tests/__init__.py
index e69de29bb2d..fb87f7b9832 100644
--- a/addons/hr_expense/tests/__init__.py
+++ b/addons/hr_expense/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_expenses \ No newline at end of file
diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py
new file mode 100644
index 00000000000..c1b53160702
--- /dev/null
+++ b/addons/hr_expense/tests/test_expenses.py
@@ -0,0 +1,74 @@
+from openerp.tests.common import TransactionCase
+from openerp import workflow
+
+
+class TestCheckJournalEntry(TransactionCase):
+ """
+ Check journal entries when the expense product is having tax which is tax included.
+ """
+
+ def setUp(self):
+ super(TestCheckJournalEntry, self).setUp()
+ cr, uid = self.cr, self.uid
+ self.expense_obj = self.registry('hr.expense.sheet')
+ self.exp_line_obj = self.registry('hr.expense')
+ self.product_obj = self.registry('product.product')
+ self.tax_obj = self.registry('account.tax')
+ _, self.product_id = self.registry("ir.model.data").get_object_reference(cr, uid, "hr_expense", "air_ticket")
+ self.employee = self.registry("ir.model.data").xmlid_to_object(cr, uid, "hr.employee_mit")
+ self.tax_id = self.tax_obj.create(cr, uid, {
+ 'name': 'Expense 10%',
+ 'amount': 10,
+ 'amount_type': 'percent',
+ 'type_tax_use': 'purchase',
+ 'price_include': True,
+ })
+ self.product_obj.write(cr, uid, self.product_id, {'supplier_taxes_id': [(6, 0, [self.tax_id])]})
+ # Create payable account for the expense
+ user_type_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'data_account_type_payable')[1]
+ account_payable_id = self.registry('account.account').create(cr, uid, {'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_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'data_account_type_expenses')[1]
+ account_expense_id = self.registry('account.account').create(cr, uid, {'code': 'X2120', 'name': 'HR Expense - Test Purchase Account', 'user_type_id': user_type_id})
+ # Assign it to the air ticket product
+ self.registry('product.product').write(cr, uid, self.product_id, {'property_account_expense_id': account_expense_id})
+ # Create Sale Journal
+ company_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company')[1]
+ self.registry('account.journal').create(cr, uid, {'name': 'Purchase Journal - Test', 'code': 'HRTPJ', 'type': 'purchase', 'company_id': company_id})
+ self.expense_id = self.expense_obj.create(cr, uid, {
+ 'name': 'Expense for John Smith',
+ 'employee_id': self.employee.id,
+ })
+ self.expense_line = self.exp_line_obj.create(cr, uid, {
+ 'name': 'Car Travel Expenses',
+ 'employee_id': self.employee.id,
+ 'product_id': self.product_id,
+ 'unit_amount': 700.00,
+ 'tax_ids': [(6, 0, [self.tax_id])],
+ 'sheet_id': self.expense_id,
+ })
+
+ def test_journal_entry(self):
+ cr, uid = self.cr, self.uid
+ self.expense = self.expense_obj.browse(cr, uid, self.expense_id)
+ #Submitted to Manager
+ self.assertEquals(self.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')
+ #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')
+
+ # [(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:
+ if line.credit:
+ self.assertAlmostEquals(line.credit, 700.00)
+ else:
+ if not line.tax_line_id.id == self.tax_id:
+ self.assertAlmostEquals(line.debit, 636.36)
+ else:
+ self.assertAlmostEquals(line.debit, 63.64) \ No newline at end of file
diff --git a/addons/hr_expense/views/hr_dashboard.xml b/addons/hr_expense/views/hr_dashboard.xml
index bf4afa7943a..26d049bfd62 100644
--- a/addons/hr_expense/views/hr_dashboard.xml
+++ b/addons/hr_expense/views/hr_dashboard.xml
@@ -7,18 +7,18 @@
<field name="arch" type="xml">
<data>
<xpath expr="//templates" position="before">
- <field name="expense_to_approve_count"/>
+ <field name="expense_sheets_to_approve_count"/>
</xpath>
<xpath expr="//div[contains(@class, 'o_kanban_primary_right')]" position="inside">
- <div t-if="record.expense_to_approve_count.raw_value > 0" class="row">
+ <div t-if="record.expense_sheets_to_approve_count.raw_value > 0" class="row">
<div class="col-xs-9">
- <a name="%(hr_expense_action_from_department)d" type="action">
- Expenses
+ <a name="%(action_hr_expense_sheet_department_to_approve)d" type="action">
+ Expense Reports
</a>
</div>
<div class="col-xs-3">
- <t t-esc="record.expense_to_approve_count.raw_value"/>
+ <t t-esc="record.expense_sheets_to_approve_count.raw_value"/>
</div>
</div>
</xpath>
@@ -26,8 +26,8 @@
<xpath expr="//div[contains(@class, 'o_kanban_manage_to_approve')]" position="inside">
<div class="row">
<div class="col-xs-12 text-left">
- <a name="%(hr_expense_action_from_department)d" type="action">
- <t t-esc="record.expense_to_approve_count.raw_value or 0"/> Expenses
+ <a name="%(action_hr_expense_sheet_department_to_approve)d" type="action">
+ <t t-esc="record.expense_sheets_to_approve_count.raw_value or 0"/> Expense Reports
</a>
</div>
</div>
@@ -36,8 +36,8 @@
<xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]" position="inside">
<div class="row">
<div class="col-xs-12 text-left">
- <a name="%(action_hr_expense_report_filtered)d" type="action">
- Expenses
+ <a name="%(action_hr_expense_sheet_department_filtered)d" type="action">
+ Expense Reports
</a>
</div>
</div>
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index cdbc60bfa3d..d2a3c55f084 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -1,41 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
-
- <!-- Expenses -->
-
<record id="view_expenses_tree" model="ir.ui.view">
<field name="name">hr.expense.tree</field>
<field name="model">hr.expense</field>
<field name="arch" type="xml">
- <tree string="Expenses" decoration-warning="state=='draft'" decoration-bf="message_unread == True">
+ <tree string="Expenses" decoration-bf="message_unread == True">
+ <field name="currency_id" invisible="True"/>
+ <field name="attachment_number" invisible="True"/>
<field name="date"/>
<field name="name"/>
<field name="employee_id"/>
+ <field name="sheet_id" invisible="not context.get('show_report', False)"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
- <field name="total_amount" sum="Total Amount" widget="monetary"/>
- <field name="state"/>
- <field name="message_unread" invisible="1"/>
- <button name="submit_expenses" states="draft" string="Submit to Manager" type="object" icon="fa-check"/>
- <button name="approve_expenses" states="submit" string="Click here to approve" type="object" icon="fa-check" groups="base.group_hr_manager"/>
- <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Click here to refuse" type="action" icon="fa-times" groups="base.group_hr_manager"/>
- </tree>
- </field>
- </record>
-
- <record id="hr_expense_my_tree" model="ir.ui.view">
- <field name="name">my.hr.expense.tree</field>
- <field name="model">hr.expense</field>
- <field name="arch" type="xml">
- <tree string="Expenses" decoration-warning="state=='draft'" decoration-bf="message_unread == True">
- <field name="date"/>
- <field name="name"/>
- <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
- <field name="total_amount" sum="Total Amount" widget="monetary"/>
+ <field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="state"/>
<field name="message_unread" invisible="1"/>
- <button name="submit_expenses" states="draft" string="Submit to Manager" type="object" icon="fa-check"/>
- <button name="approve_expenses" states="submit" string="Click here to approve" type="object" icon="fa-check" groups="base.group_hr_manager"/>
- <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Click here to refuse" type="action" icon="fa-times" groups="base.group_hr_manager"/>
+ <button name="action_get_attachment_view" string="View Attached Documents" type="object" icon="fa-paperclip" attrs="{'invisible': [('attachment_number', '=', 0)]}"/>
</tree>
</field>
</record>
@@ -48,12 +28,8 @@
<form string="Expenses">
<header>
<button name="submit_expenses" states="draft" string="Submit to Manager" type="object" class="oe_highlight"/>
- <button name="approve_expenses" states="submit" string="Approve" type="object" groups="base.group_hr_user" class="oe_highlight"/>
- <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Refuse" type="action" groups="base.group_hr_user" />
- <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="approve" string="Refuse" type="action" groups="account.group_account_user" />
- <button name="reset_expenses" states="cancel" string="Set to Draft" type="object"/>
- <button name="action_move_create" states="approve" string="Post Journal Entries" type="object" groups="account.group_account_user" class="oe_highlight"/>
- <field name="state" widget="statusbar" statusbar_visible="draft,submit,approve,post,done" statusbar_colors='{"submit":"blue","cancel":"red"}'/>
+ <field name="state" widget="statusbar" statusbar_visible="draft,reported,done" statusbar_colors='{"reported":"blue"}'/>
+ <button name="view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
</header>
<sheet>
<div class="oe_button_box">
@@ -67,7 +43,7 @@
<div class="oe_title">
<label for="name"/>
<h1>
- <field name="name" placeholder="e.g. Business lunch with X"/>
+ <field name="name" placeholder="e.g. Business Trip NY"/>
</h1>
</div>
<group>
@@ -79,27 +55,21 @@
<field name="quantity" class="oe_inline"/>
<field name="product_uom_id" widget="selection" class="oe_inline" groups="product.group_uom"/>
</div>
- <field name="tax_ids" widget="many2many_tags" groups="account.group_account_user"/>
+ <field name="tax_ids" domain="[('type_tax_use', '=', 'purchase')]" widget="many2many_tags" groups="account.group_no_one"/>
</group><group>
+ <field name="reference"/>
<field name="date"/>
- <field name="accounting_date"/>
+ <field name="account_id"/>
<field name="employee_id"/>
+ <field name="sheet_id" invisible="1"/>
<field name="currency_id" groups="base.group_multi_currency"/>
- <field name="department_id" invisible="1"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="company_id" groups="base.group_multi_company"/>
- <field name="journal_id" domain="[('type', '=', 'purchase')]" groups="account.group_account_user"
- options="{'no_open': True, 'no_create': True}"/>
- <field name="account_id" groups="account.group_account_user"/>
- <field name="account_move_id" domain="[('type', '=', 'purchase'])]" readonly="1"/>
</group><group>
<field name="total_amount" widget='monetary' options="{'currency_field': 'currency_id'}"/>
<label for="payment_mode"/>
<div>
<field name="payment_mode" widget="radio"/>
- <field name="bank_journal_id"
- attrs="{'invisible': [('payment_mode', '=', 'own_account')], 'required': [('payment_mode', '=', 'company_account')]}" domain="[('type', 'in', ['cash', 'bank'])]"
- widget="selection"/>
</div>
</group>
@@ -187,21 +157,21 @@
<field name="name" string="Expenses"/>
<field name="date"/>
<field name="employee_id"/>
- <field name="department_id" string="Department" context="{'invisible_department': False}"/>
<field name="product_id"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<separator />
- <filter domain="[('state', '=', 'draft')]" string="New" help="New Expense"/>
- <filter domain="[('state', '=', 'submit')]" string="To Approve" name="submitted" help="Confirmed Expenses"/>
- <filter domain="['|', ('state', '=', 'approve'), ('state', '=', 'post')]" string="To Pay" name="approved" help="Expenses to Invoice"/>
+ <filter domain="[('state', '=', 'draft')]" string="To Report" name="to_report" help="New Expense"/>
+ <filter domain="[('state', '=', 'report')]" string="Reported" name="submitted" help="Confirmed Expenses"/>
+ <filter domain="[('state', '=', 'refused')]" string="Refused" name="refused" help="Refused Expenses"/>
+ <filter domain="[('state', '!=', 'cancel')]" string="Not Refused" name="uncancelled" help="Actual expense sheets, not the refused ones"/>
<separator />
<filter string="My Team Expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="base.group_hr_manager" help="Expenses of Your Team Member"/>
<filter string="My Expenses" domain="[('employee_id.user_id', '=', uid)]"/>
<separator />
<filter string="New Mail" name="message_unread" domain="[('message_unread', '=', True)]"/>
<group expand="0" string="Group By">
+ <filter string="Report" domain="[]" context="{'group_by': 'sheet_id'}"/>
<filter string="Employee" domain="[]" context="{'group_by': 'employee_id'}"/>
- <filter string="Department" domain="[]" context="{'group_by': 'department_id'}" groups="hr.group_multi_departments"/>
<filter string="Company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
<filter string="Product" domain="[]" context="{'group_by': 'product_id'}"/>
<filter string="Analytic Account" domain="[]" context="{'group_by': 'analytic_account_id'}" groups="analytic.group_analytic_accounting"/>
@@ -214,12 +184,43 @@
</field>
</record>
- <record id="expense_all" model="ir.actions.act_window">
+ <record id="hr_expense_actions_all" model="ir.actions.act_window">
+ <field name="name">All Expenses</field>
+ <field name="res_model">hr.expense</field>
+ <field name="view_mode">tree,kanban,form,graph,pivot</field>
+ <field name="search_view_id" ref="view_hr_expense_filter"/>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create new expenses.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
+ </record>
+
+ <record id="hr_expense_actions_my_all" model="ir.actions.act_window">
<field name="name">My Expenses</field>
<field name="res_model">hr.expense</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
+ <field name="context">{'show_report': True}</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create new expenses.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
+ </record>
+
+ <record id="hr_expense_actions_my_unsubmitted" model="ir.actions.act_window">
+ <field name="name">My Expenses</field>
+ <field name="res_model">hr.expense</field>
+ <field name="view_mode">tree,kanban,form,graph,pivot</field>
+ <field name="search_view_id" ref="view_hr_expense_filter"/>
+ <field name="domain">[('employee_id.user_id', '=', uid)]</field>
+ <field name="context">{'search_default_to_report': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create new expenses.
@@ -232,22 +233,22 @@
<record id="hr_expense_tree_action" model="ir.actions.act_window.view">
<field name="sequence" eval="0"/>
<field name="view_mode">tree</field>
- <field name="view_id" ref="hr_expense_my_tree"/>
- <field name="act_window_id" ref="expense_all"/>
+ <field name="view_id" ref="view_expenses_tree"/>
+ <field name="act_window_id" ref="hr_expense_actions_all"/>
</record>
<record id="hr_expense_kanban_action" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="hr_expense_kanban_view"/>
- <field name="act_window_id" ref="expense_all"/>
+ <field name="act_window_id" ref="hr_expense_actions_all"/>
</record>
<record id="hr_expense_form_action" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="hr_expense_form_view"/>
- <field name="act_window_id" ref="expense_all"/>
+ <field name="act_window_id" ref="hr_expense_actions_all"/>
</record>
<record id="view_product_hr_expense_form" model="ir.ui.view">
@@ -293,105 +294,428 @@
</field>
</record>
- <record id="action_request_approve_expense" model="ir.actions.act_window">
- <field name="name">Expenses to Approve</field>
+ <record id="action_approved_expense" model="ir.actions.act_window">
+ <field name="name">Unsubmitted Expenses</field>
<field name="res_model">hr.expense</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form,pivot,graph</field>
- <field name="context">{'search_default_submitted': 1, 'needaction_menu_ref': 'hr_expense.menu_expense_all'}</field>
- <field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="view_id" ref="view_expenses_tree"/>
+ <field name="domain">[]</field>
+ <field name="context">{"search_default_unsubmitted": 1}</field>
+ <field name="help" type="html">
+ <p>
+ From here the accountant will be able to approve as well as refuse the expenses which are verified by the HR Manager.
+ </p>
+ </field>
+ </record>
+
+ <record id="hr_expense_action" model="ir.actions.act_window">
+ <field name="name">Expenses Analysis</field>
+ <field name="res_model">hr.expense</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">pivot,graph</field>
+ <field name="context">{
+ 'search_default_uncancelled': 1,
+ }
+ </field>
+ </record>
+
+ <record id="view_hr_expense_sheet_tree" model="ir.ui.view">
+ <field name="name">hr.expense.sheet.tree</field>
+ <field name="model">hr.expense.sheet</field>
+ <field name="arch" type="xml">
+ <tree string="Expense Reports" decoration-warning="state=='draft'" decoration-bf="message_unread == True">
+ <field name="accounting_date"/>
+ <field name="name" string="Expense Sheet"/>
+ <field name="employee_id"/>
+ <field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
+ <field name="state"/>
+ <field name="message_unread" invisible="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_hr_expense_sheet_form" model="ir.ui.view">
+ <field name="name">hr.expense.sheet.form</field>
+ <field name="model">hr.expense.sheet</field>
+ <field eval="25" name="priority"/>
+ <field name="arch" type="xml">
+ <form string="Expense Reports">
+ <header>
+ <button name="approve_expense_sheets" states="submit" string="Approve" type="object" groups="base.group_hr_user" class="oe_highlight"/>
+ <button name="action_sheet_move_create" states="approve" string="Post Journal Entries" type="object" groups="account.group_account_user" class="oe_highlight"/>
+ <button name="%(hr_expense.hr_expense_register_payment_wizard_action)d" type="action" string="Register Payment" class="oe_highlight" attrs="{'invisible': [('state', '!=', 'post')]}" context="{'default_amount': total_amount, 'partner_id': address_id}" groups="account.group_account_user"/>
+ <button name="reset_expense_sheets" states="cancel" string="Resubmit" type="object"/>
+ <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="submit" string="Refuse" type="action" groups="base.group_hr_user" />
+ <button name="%(hr_expense.hr_expense_refuse_wizard_action)d" states="approve" string="Refuse" type="action" groups="account.group_account_user"/>
+ <field name="state" widget="statusbar" statusbar_visible="draft,submit,approve,post,done" statusbar_colors='{"submit":"blue","cancel":"red"}'/>
+ </header>
+ <sheet>
+ <div class="oe_button_box">
+ <button name="action_get_attachment_view"
+ class="oe_stat_button"
+ icon="fa-book"
+ type="object">
+ <field name="attachment_number" widget="statinfo" string="Documents"/>
+ </button>
+ </div>
+ <div class="oe_title">
+ <label for="name" class="oe_edit_only"/>
+ <h1>
+ <field name="name" placeholder="e.g. Business lunch with X"/>
+ </h1>
+ </div>
+ <group>
+ <group>
+ <field name="employee_id"/>
+ <field name="payment_mode"/>
+ <field name="responsible_id" invisible="1"/>
+ <field name="total_amount" invisible="1"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="address_id" invisible="1"/>
+ <field name="department_id" invisible="1"/>
+ <field name="company_id" groups="base.group_multi_company"/>
+ </group>
+ <group>
+ <field name="journal_id" domain="[('type', '=', 'purchase')]" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}"/>
+ <field name="bank_journal_id" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'company_account')]}"/>
+ <field name="account_id" groups="account.group_account_user"/>
+ <field name="accounting_date" groups="account.group_account_user" attrs="{'invisible': [('state', 'not in', ['approve', 'post', 'done'])]}"/>
+ <field name="account_move_id" attrs="{'invisible': [('state', 'not in', ['post', 'done'])]}" groups="account.group_account_user"/>
+ </group>
+ </group>
+ <field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'no_delete': True}">
+ <tree>
+ <field name="date"/>
+ <field name="name"/>
+ <field name="state" invisible="1"/>
+ <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
+ <field name="total_amount" sum="Total Amount" widget="monetary"/>
+ <field name="message_unread" invisible="1"/>
+ <field name="attachment_number" invisible="1"/>
+ <field name="tax_ids" widget="many2many_tags"/>
+ <field name="attachment_number" string=" "/>
+ <button name="action_get_attachment_view" string="View Attachments" type="object" icon="fa-paperclip"/>
+ </tree>
+ </field>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_hr_expense_sheet_kanban" model="ir.ui.view">
+ <field name="name">hr.expense.sheet.kanban</field>
+ <field name="model">hr.expense.sheet</field>
+ <field name="arch" type="xml">
+ <kanban class="o_kanban_mobile">
+ <field name="name"/>
+ <field name="employee_id"/>
+ <field name="total_amount"/>
+ <field name="accounting_date"/>
+ <field name="state"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div t-attf-class="oe_kanban_card oe_kanban_global_click">
+ <div class="row">
+ <div class="col-xs-6">
+ <strong><span><t t-esc="record.name.value"/></span></strong>
+ </div>
+ <div class="col-xs-6">
+ <strong><span class="pull-right text-right"><t t-esc="record.total_amount.value"/></span></strong>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xs-6 text-muted">
+ <span><t t-esc="record.employee_id.value"/> <t t-esc="record.date.value"/></span>
+ </div>
+ <div class="col-xs-6">
+ <span class="pull-right text-right">
+ <field name="state" widget="kanban_label_selection" options="{'classes': {'draft': 'default', 'submit': 'default', 'cancel': 'danger', 'post': 'warning',
+ 'done': 'success'}}"/>
+ </span>
+ </div>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="view_hr_expense_sheet_pivot" model="ir.ui.view">
+ <field name="name">hr.expense.sheet.pivot</field>
+ <field name="model">hr.expense.sheet</field>
+ <field name="arch" type="xml">
+ <pivot string="Expenses Analysis" disable_linking="True">
+ <field name="employee_id" type="row"/>
+ <field name="accounting_date" interval="month" type="col"/>
+ <field name="total_amount" type="measure"/>
+ </pivot>
+ </field>
+ </record>
+
+ <record id="view_hr_expense_sheet_graph" model="ir.ui.view">
+ <field name="name">hr.expense.sheet.graph</field>
+ <field name="model">hr.expense.sheet</field>
+ <field name="arch" type="xml">
+ <graph string="Expenses Analysis">
+ <field name="employee_id" type="col"/>
+ <field name="accounting_date" interval="month" type="row"/>
+ <field name="total_amount" type="measure"/>
+ </graph>
+ </field>
+ </record>
+
+
+ <record id="view_hr_expense_sheet_filter" model="ir.ui.view">
+ <field name="name">hr.expense.sheet.filter</field>
+ <field name="model">hr.expense.sheet</field>
+ <field name="arch" type="xml">
+ <search string="Expense">
+ <field name="name" string="Expenses"/>
+ <field name="accounting_date"/>
+ <field name="state"/>
+ <field name="employee_id"/>
+ <field name="department_id" string="Department"/>
+ <separator />
+ <filter domain="[('state', '=', 'submit')]" string="To Approve" name="submitted" help="Confirmed Expenses"/>
+ <filter domain="[('state', '=', 'approve')]" string="To Post" name="to_post" help="Approved Expenses"/>
+ <filter domain="['|', ('state', '=', 'approve'), ('state', '=', 'post')]" string="To Pay" name="approved" help="Expenses to Invoice"/>
+ <separator/>
+ <filter string="My Team Reports" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="base.group_hr_manager" help="Expenses of Your Team Member"/>
+ <filter string="My Reports" domain="[('employee_id.user_id', '=', uid)]"/>
+ <separator />
+ <filter string="New Mail" name="message_unread" domain="[('message_unread', '=', True)]"/>
+ <group expand="0" string="Group By">
+ <filter string="Employee" domain="[]" context="{'group_by': 'employee_id'}"/>
+ <filter string="Department" domain="[]" context="{'group_by': 'department_id'}" groups="hr.group_multi_departments"/>
+ <filter string="Company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
+ <filter string="Expenses Month" domain="[]" context="{'group_by': 'accounting_date'}" help="Expenses by Month"/>
+ </group>
+ <separator />
+ <filter domain="[('employee_id.active', '=', False)]" string="Former Employees" name="inactive" groups="base.group_hr_user,base.group_hr_manager"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="action_hr_expense_sheet_my_to_approve" model="ir.actions.act_window">
+ <field name="name">My Reports</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="domain">[('employee_id.user_id', '=', uid)]</field>
+ <field name="context">{'search_default_submitted': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
- Click here to create new expenses.
+ Click here to create a new expense report.
</p><p>
Once you have created your expense, submit it to your manager who will validate it.
</p>
</field>
</record>
- <record id="hr_expense_submit_action_server" model="ir.actions.server">
- <field name="name">Submit Expenses</field>
- <field name="type">ir.actions.server</field>
- <field name="model_id" ref="model_hr_expense"/>
- <field name="state">code</field>
- <field name="code">
- self.submit_expenses(cr, uid, context.get('active_ids', []), context=context)
+ <record id="action_hr_expense_sheet_my_all" model="ir.actions.act_window">
+ <field name="name">My Reports</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="domain">[('employee_id.user_id', '=', uid)]</field>
+ <field name="context">{}</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new expense report.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
</field>
</record>
- <record id="action_hr_expense_submit" model="ir.values">
- <field name="name">action_hr_expense_submit</field>
- <field name="action_id" ref="hr_expense_submit_action_server" />
- <field name="value" eval="'ir.actions.server,' + str(ref('hr_expense_submit_action_server'))" />
- <field name="key">action</field>
- <field name="model_id" ref="model_hr_expense"/>
- <field name="model">hr.expense</field>
- <field name="key2">client_action_multi</field>
+ <record id="action_hr_expense_sheet_all_to_approve" model="ir.actions.act_window">
+ <field name="name">My Reports</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="domain">[]</field>
+ <field name="context">{'search_default_submitted': 1}</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new expense report.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
</record>
- <record id="hr_expense_entry_action_server" model="ir.actions.server">
- <field name="name">Generate Accounting Entries</field>
- <field name="type">ir.actions.server</field>
- <field name="model_id" ref="model_hr_expense"/>
- <field name="state">code</field>
- <field name="code">
- self.action_move_create(cr, uid, context.get('active_ids', []), context=context)
+ <record id="action_hr_expense_sheet_all_to_post" model="ir.actions.act_window">
+ <field name="name">My Reports</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="domain">[]</field>
+ <field name="context">{'search_default_to_post': 1}</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new expense report.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
</field>
</record>
- <record id="action_hr_expense_account_entry" model="ir.values">
- <field name="name">action_hr_expense_account_entry</field>
- <field name="action_id" ref="hr_expense_entry_action_server" />
- <field name="value" eval="'ir.actions.server,' + str(ref('hr_expense_entry_action_server'))" />
- <field name="key">action</field>
- <field name="model_id" ref="model_hr_expense"/>
- <field name="model">hr.expense</field>
- <field name="key2">client_action_multi</field>
+ <record id="action_hr_expense_sheet_all_to_pay" model="ir.actions.act_window">
+ <field name="name">My Reports</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="domain">[]</field>
+ <field name="context">{'search_default_approved': 1}</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new expense report.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
</record>
- <menuitem id="menu_expense_approved" parent="account.menu_finance_payables"
- action="action_approved_expense" sequence="15"/>
- <menuitem id="menu_hr_expense_root" name="Expenses" sequence="100" web_icon="hr_expense,static/description/icon.png"/>
- <menuitem id="menu_hr_expense_configuration" name="Configuration" parent="menu_hr_expense_root"
- sequence="100"/>
- <menuitem id="menu_hr_product" name="Expense Products" parent="menu_hr_expense_configuration"
- action="hr_expense_product" groups="base.group_hr_manager"/>
- <menuitem id="menu_expense_all" action="expense_all" name="My Expenses" parent="menu_hr_expense_root"/>
- <menuitem id="menu_expense_to_approve" action="action_request_approve_expense" name="To Approve" parent="menu_hr_expense_root" groups="base.group_hr_user"/>
+ <record id="action_hr_expense_sheet_all_all" model="ir.actions.act_window">
+ <field name="name">My Reports</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="domain">[]</field>
+ <field name="context">{}</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new expense report.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
+ </record>
- <record id="hr_expense_action_from_department" model="ir.actions.act_window">
- <field name="name">Expenses to Approve</field>
- <field name="res_model">hr.expense</field>
+ <record id="action_request_approve_expense_sheet" model="ir.actions.act_window">
+ <field name="name">Reports to Approve</field>
+ <field name="res_model">hr.expense.sheet</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form,pivot,graph</field>
- <field name="search_view_id" ref="view_hr_expense_filter"/>
+ <field name="context">{'search_default_submitted': 1, 'needaction_menu_ref': 'hr_expense.menu_hr_expense_all'}</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="view_id" ref="view_hr_expense_sheet_tree"/>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create new expenses.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
+ </record>
+
+ <record id="action_request_to_post_expense_sheet" model="ir.actions.act_window">
+ <field name="name">Reports to Post</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="context">{'search_default_to_post': 1, 'needaction_menu_ref': 'hr_expense.menu_hr_expense_all'}</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="view_id" ref="view_hr_expense_sheet_tree"/>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create new expenses.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
+ </record>
+
+ <record id="action_request_to_pay_expense_sheet" model="ir.actions.act_window">
+ <field name="name">Reports to Pay</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="context">{'search_default_approved': 1, 'needaction_menu_ref': 'hr_expense.menu_hr_expense_all'}</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
+ <field name="view_id" ref="view_hr_expense_sheet_tree"/>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create new expenses.
+ </p><p>
+ Once you have created your expense, submit it to your manager who will validate it.
+ </p>
+ </field>
+ </record>
+
+ <record id="action_hr_expense_sheet_department_to_approve" model="ir.actions.act_window">
+ <field name="name">Expense Reports to Approve</field>
+ <field name="res_model">hr.expense.sheet</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,kanban,form,pivot,graph</field>
+ <field name="search_view_id" ref="view_hr_expense_sheet_filter"/>
<field name="context">{
'search_default_submitted': 1,
'search_default_department_id': [active_id],
- 'default_department_id': active_id}
+ 'default_department_id': active_id
+ }
</field>
</record>
- <record id="hr_expense_action" model="ir.actions.act_window">
- <field name="name">Expenses Analysis</field>
- <field name="res_model">hr.expense</field>
+ <record id="action_hr_expense_sheet_department_filtered" model="ir.actions.act_window">
+ <field name="name">Expense Reports Analysis</field>
+ <field name="res_model">hr.expense.sheet</field>
<field name="view_type">form</field>
- <field name="view_mode">pivot,graph,tree,form,kanban</field>
+ <field name="view_mode">pivot,graph</field>
+ <field name="context">{
+ 'search_default_department_id': [active_id],
+ 'default_department_id': active_id}
+ </field>
</record>
- <menuitem id="menu_hr_expense" name="Reports" sequence="99" parent="menu_hr_expense_root" action="hr_expense_action"/>
+ <record id="hr_expense_submit_action_server" model="ir.actions.server">
+ <field name="name">Submit To Manager</field>
+ <field name="type">ir.actions.server</field>
+ <field name="model_id" ref="model_hr_expense"/>
+ <field name="state">code</field>
+ <field name="code">
+ if context.get('active_model') == 'hr.expense' and context.get('active_ids'):
+ action = env['hr.expense'].browse(context['active_ids']).submit_expenses()
+ </field>
+ </record>
+ <record id="action_hr_expense_submit" model="ir.values">
+ <field name="name">Submit To Manager</field>
+ <field name="model_id" ref="model_hr_expense"/>
+ <field name="model">hr.expense</field>
+ <field name="key2">client_action_multi</field>
+ <field eval="'ir.actions.server,%d'%hr_expense_submit_action_server" name="value"/>
+ </record>
+
+ <menuitem id="menu_hr_expense_root" name="Expenses" sequence="100" web_icon="hr_expense,static/description/icon.png"/>
+
+ <menuitem id="menu_hr_expense_my_expenses" name="My Expenses" sequence="10" parent="menu_hr_expense_root"/>
+ <menuitem id="menu_hr_expense_my_unsubmitted" name="Unsubmitted Expenses" sequence="1" parent="menu_hr_expense_my_expenses" action="hr_expense_actions_my_unsubmitted"/>
+ <menuitem id="menu_hr_expense_my_all" name="All Expenses" sequence="2" parent="menu_hr_expense_my_expenses"
+ action="hr_expense_actions_my_all"/>
- <record id="action_hr_expense_report_filtered" model="ir.actions.act_window">
- <field name="name">Expenses Analysis</field>
- <field name="res_model">hr.expense</field>
- <field name="view_type">form</field>
- <field name="view_mode">pivot,graph</field>
- <field name="context">{
- 'search_default_department_id': [active_id],
- 'default_department_id': active_id}
- </field>
- </record>
+ <menuitem id="menu_hr_expense_sheet_my_reports" name="My Reports" sequence="15" parent="menu_hr_expense_root"/>
+ <menuitem id="menu_hr_expense_sheet_my_to_approve" name="To Approve" sequence="1" parent="menu_hr_expense_sheet_my_reports" action="action_hr_expense_sheet_my_to_approve"/>
+ <menuitem id="menu_hr_expense_sheet_my_all" name="All Reports" sequence="2" parent="menu_hr_expense_sheet_my_reports" action="action_hr_expense_sheet_my_all"/>
+ <menuitem id="menu_hr_expense_sheet_all" name="All Reports" sequence="20" parent="menu_hr_expense_root"/>
+ <menuitem id="menu_hr_expense_sheet_all_to_approve" name="To Approve" sequence="1" parent="menu_hr_expense_sheet_all"
+ action="action_hr_expense_sheet_all_to_approve" groups="base.group_hr_user"/>
+ <menuitem id="menu_hr_expense_sheet_all_to_post" name="To Post" sequence="2" parent="menu_hr_expense_sheet_all"
+ action="action_hr_expense_sheet_all_to_post" groups="base.group_hr_manager"/>
+ <menuitem id="menu_hr_expense_sheet_all_to_pay" name="To Pay" sequence="3" parent="menu_hr_expense_sheet_all"
+ action="action_hr_expense_sheet_all_to_pay" groups="base.group_hr_manager"/>
+ <menuitem id="menu_hr_expense_sheet_all_all" name="All" sequence="4" parent="menu_hr_expense_sheet_all"
+ action="action_hr_expense_sheet_all_all" groups="base.group_hr_manager"/>
+
+ <menuitem id="menu_hr_expense_configuration" name="Configuration" parent="menu_hr_expense_root"
+ sequence="100"/>
+ <menuitem id="menu_hr_product" name="Expense Products" parent="menu_hr_expense_configuration"
+ action="hr_expense_product" groups="base.group_hr_manager" sequence="10"/>
</odoo>
diff --git a/addons/hr_expense/wizard/__init__.py b/addons/hr_expense/wizard/__init__.py
index bc8a5c091e0..faafe22de6b 100644
--- a/addons/hr_expense/wizard/__init__.py
+++ b/addons/hr_expense/wizard/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import hr_expense_refuse_reason
+import hr_expense_register_payment
diff --git a/addons/hr_expense/wizard/hr_expense_refuse_reason.py b/addons/hr_expense/wizard/hr_expense_refuse_reason.py
index c317c026fe5..c223d73a1a1 100644
--- a/addons/hr_expense/wizard/hr_expense_refuse_reason.py
+++ b/addons/hr_expense/wizard/hr_expense_refuse_reason.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import api, fields, models
+from odoo import api, fields, models
class HrExpenseRefuseWizard(models.TransientModel):
@@ -17,6 +17,6 @@ class HrExpenseRefuseWizard(models.TransientModel):
context = dict(self._context or {})
active_ids = context.get('active_ids', [])
- expense = self.env['hr.expense'].browse(active_ids)
- expense.refuse_expenses(self.description)
+ expense_sheet = self.env['hr.expense.sheet'].browse(active_ids)
+ expense_sheet.refuse_expenses(self.description)
return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/hr_expense/wizard/hr_expense_register_payment.py b/addons/hr_expense/wizard/hr_expense_register_payment.py
new file mode 100644
index 00000000000..05f0038a7da
--- /dev/null
+++ b/addons/hr_expense/wizard/hr_expense_register_payment.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from openerp.exceptions import ValidationError
+
+
+class HrExpenseRegisterPaymentWizard(models.TransientModel):
+
+ _name = "hr.expense.register.payment.wizard"
+ _description = "Hr Expense Register Payment wizard"
+
+ @api.model
+ def _default_partner_id(self):
+ context = dict(self._context or {})
+ active_ids = context.get('active_ids', [])
+ expense_sheet = self.env['hr.expense.sheet'].browse(active_ids)
+ return expense_sheet.address_id.id or expense_sheet.employee_id.id and expense_sheet.employee_id.address_home_id.id
+
+ partner_id = fields.Many2one('res.partner', string='Partner', required=True, default=_default_partner_id)
+ journal_id = fields.Many2one('account.journal', string='Payment Method', required=True, domain=[('type', 'in', ('bank', 'cash'))])
+ company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', readonly=True, required=True)
+ payment_method_id = fields.Many2one('account.payment.method', string='Payment Type', required=True)
+ amount = fields.Monetary(string='Payment Amount', required=True)
+ currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.user.company_id.currency_id)
+ payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True)
+ communication = fields.Char(string='Memo')
+ hide_payment_method = fields.Boolean(compute='_compute_hide_payment_method',
+ help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'")
+
+ @api.one
+ @api.constrains('amount')
+ def _check_amount(self):
+ if not self.amount > 0.0:
+ raise ValidationError('The payment amount must be strictly positive.')
+
+ @api.one
+ @api.depends('journal_id')
+ def _compute_hide_payment_method(self):
+ if not self.journal_id:
+ self.hide_payment_method = True
+ return
+ journal_payment_methods = self.journal_id.outbound_payment_method_ids
+ self.hide_payment_method = len(journal_payment_methods) == 1 and journal_payment_methods[0].code == 'manual'
+
+ @api.onchange('journal_id')
+ def _onchange_journal(self):
+ if self.journal_id:
+ # Set default payment method (we consider the first to be the default one)
+ payment_methods = self.journal_id.outbound_payment_method_ids
+ self.payment_method_id = payment_methods and payment_methods[0] or False
+ # Set payment method domain (restrict to methods enabled for the journal and to selected payment type)
+ return {'domain': {'payment_method_id': [('payment_type', '=', 'outbound'), ('id', 'in', payment_methods.ids)]}}
+ return {}
+
+ @api.multi
+ def expense_post_payment(self):
+ self.ensure_one()
+ context = dict(self._context or {})
+ active_ids = context.get('active_ids', [])
+ expense_sheet = self.env['hr.expense.sheet'].browse(active_ids)
+
+ # Create payment and post it
+ payment = self.env['account.payment'].create({
+ 'partner_type': 'supplier',
+ 'payment_type': 'outbound',
+ 'partner_id': self.partner_id.id,
+ 'journal_id': self.journal_id.id,
+ 'company_id': self.company_id.id,
+ 'payment_method_id': self.payment_method_id.id,
+ 'amount': self.amount,
+ 'currency_id': self.currency_id.id,
+ 'payment_date': self.payment_date,
+ 'communication': self.communication
+ })
+ payment.post()
+
+ # Log the payment in the chatter
+ body = (_("A payment of %s %s with the reference %s related to your expense %s has been made.") % (payment.amount, payment.currency_id.symbol, payment.name, expense_sheet.name))
+ expense_sheet.message_post(body=body)
+
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/hr_expense/wizard/hr_expense_register_payment.xml b/addons/hr_expense/wizard/hr_expense_register_payment.xml
new file mode 100644
index 00000000000..d8ae5342555
--- /dev/null
+++ b/addons/hr_expense/wizard/hr_expense_register_payment.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="hr_expense_register_payment_view_form" model="ir.ui.view">
+ <field name="name">hr.expense.register.payment.wizard.form</field>
+ <field name="model">hr.expense.register.payment.wizard</field>
+ <field name="arch" type="xml">
+ <form string="Register Payment">
+ <sheet>
+ <field name="id" invisible="1"/>
+ <div class="oe_title">
+ <h1>Draft Payment</h1>
+ </div>
+ <group>
+ <group>
+ <field name="partner_id" required="1" context="{'default_is_company': True, 'default_supplier': True}"/>
+ <field name="journal_id" widget="selection"/>
+ <field name="hide_payment_method" invisible="1"/>
+ <field name="payment_method_id" widget="radio" attrs="{'invisible': [('hide_payment_method', '=', True)]}"/>
+ <label for="amount"/>
+ <div name="amount_div" class="o_row">
+ <field name="amount"/>
+ <field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
+ </div>
+ </group>
+ <group>
+ <field name="payment_date"/>
+ <field name="communication"/>
+ </group>
+ </group>
+ </sheet>
+ <footer>
+ <button string='Validate' name="expense_post_payment" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-default" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <!-- Expenses -->
+ <record id="hr_expense_register_payment_wizard_action" model="ir.actions.act_window">
+ <field name="name">Register Payment</field>
+ <field name="res_model">hr.expense.register.payment.wizard</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="hr_expense_register_payment_view_form"/>
+ <field name="target">new</field>
+ <field name="context">{'default_payment_type': 'inbound'}</field>
+ <field name="domain">[('partner_type', '=', 'customer')]</field>
+ </record>
+
+</odoo> \ No newline at end of file
diff --git a/addons/sale_expense/tests/test_sale_expense.py b/addons/sale_expense/tests/test_sale_expense.py
index a0da6ae48cb..0152f4df845 100644
--- a/addons/sale_expense/tests/test_sale_expense.py
+++ b/addons/sale_expense/tests/test_sale_expense.py
@@ -27,20 +27,24 @@ class TestSaleExpense(TestSale):
account_payable = self.env['account.account'].create({'code': 'X1111', 'name': 'HR Expense - Test Payable Account', 'user_type_id': self.env.ref('account.data_account_type_payable').id, 'reconcile': True})
employee = self.env['hr.employee'].create({'name': 'Test employee', 'user_id': self.user.id, 'address_home_id': self.user.partner_id.id})
self.user.partner_id.property_account_payable_id = account_payable.id
+ # Submit to Manager
+ sheet = self.env['hr.expense.sheet'].create({
+ 'name': 'Expense for John Smith',
+ 'employee_id': employee.id,
+ 'journal_id': journal.id,
+ })
exp = self.env['hr.expense'].create({
'name': 'Air Travel',
'product_id': prod_exp_1.id,
'analytic_account_id': so.project_id.id,
'unit_amount': 621.54,
- 'journal_id': journal.id,
'employee_id': employee.id,
+ 'sheet_id': sheet.id
})
- # Submit to Manager
- exp.submit_expenses()
# Approve
- exp.approve_expenses()
+ sheet.approve_expense_sheets()
# Create Expense Entries
- exp.action_move_create()
+ sheet.action_sheet_move_create()
# expense should now be in sales order
self.assertTrue(prod_exp_1 in map(lambda so: so.product_id, so.order_line), 'Sale Expense: expense product should be in so')
sol = so.order_line.filtered(lambda sol: sol.product_id.id == prod_exp_1.id)
@@ -50,6 +54,12 @@ class TestSaleExpense(TestSale):
# create some expense and validate it (expense at sales price)
init_price = so.amount_total
prod_exp_2 = self.env.ref('hr_expense.car_travel')
+ # Submit to Manager
+ sheet = self.env['hr.expense.sheet'].create({
+ 'name': 'Expense for John Smith',
+ 'employee_id': employee.id,
+ 'journal_id': journal.id,
+ })
exp = self.env['hr.expense'].create({
'name': 'Car Travel',
'product_id': prod_exp_2.id,
@@ -57,15 +67,13 @@ class TestSaleExpense(TestSale):
'product_uom_id': self.env.ref('product.product_uom_km').id,
'unit_amount': 0.15,
'quantity': 100,
- 'journal_id': journal.id,
'employee_id': employee.id,
+ 'sheet_id': sheet.id
})
- # Submit to Manager
- exp.submit_expenses()
# Approve
- exp.approve_expenses()
+ sheet.approve_expense_sheets()
# Create Expense Entries
- exp.action_move_create()
+ sheet.action_sheet_move_create()
# expense should now be in sales order
self.assertTrue(prod_exp_2 in map(lambda so: so.product_id, so.order_line), 'Sale Expense: expense product should be in so')
sol = so.order_line.filtered(lambda sol: sol.product_id.id == prod_exp_2.id)