[MERGE] hr_expense: module rewritten for more easiness and a better usability. Concept of expense sheet removed. Was PR #7387.

This commit is contained in:
Foram Katharotiya
2015-09-04 17:23:19 +02:00
committed by qdp-odoo
parent 4ce20c449f
commit 52d72e61de
37 changed files with 1070 additions and 1387 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hr_department
import hr_expense
import models
import report
import wizard
+14 -19
View File
@@ -4,7 +4,7 @@
{
'name': 'Expense Tracker',
'version': '1.0',
'version': '2.0',
'category': 'Human Resources',
'sequence': 95,
'summary': 'Expenses Validation, Invoicing',
@@ -19,34 +19,29 @@ Employee can encode their own expenses and the validation flow puts it automatic
The whole flow is implemented as:
---------------------------------
* Draft expense
* Confirmation of the sheet by the employee
* Validation by his manager
* Submitted by the employee to his manager
* Approved by his manager
* Validation by the accountant and accounting entries creation
This module also uses analytic accounting and is compatible with the invoice on timesheet module so that you are able to automatically re-invoice your customers' expenses if your work by project.
""",
'author': 'Odoo S.A.',
'website': 'https://www.odoo.com/page/expenses',
'depends': ['hr', 'account_accountant', 'report'],
'depends': ['hr_contract', 'account_accountant', 'report'],
'data': [
'security/ir.model.access.csv',
'hr_expense_data.xml',
'hr_expense_sequence.xml',
'hr_expense_workflow.xml',
'hr_expense_view.xml',
'hr_expense_report.xml',
'data/hr_expense_data.xml',
'data/hr_expense_sequence.xml',
'wizard/hr_expense_refuse_reason.xml',
'views/hr_expense_views.xml',
'security/ir_rule.xml',
'report/hr_expense_report_view.xml',
'hr_expense_installer_view.xml',
'views/hr_expense_installer_views.xml',
'views/report_expense.xml',
'hr_dashboard.xml',
],
'demo': ['hr_expense_demo.xml'],
'test': [
'../account/test/account_minimal_test.xml',
'test/expense_demo.yml',
'test/expense_process.yml',
'report/hr_expense_report_view.xml',
'data/web_tip_data.xml',
'views/hr_dashboard.xml',
],
'demo': ['data/hr_expense_demo.xml'],
'installable': True,
'auto_install': False,
'application': True,
}
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<!-- notify all employees of module installation -->
<record model="mail.message" id="module_install_notification">
<record id="module_install_notification" model="mail.message">
<field name="model">mail.channel</field>
<field name="res_id" ref="mail.channel_all_employees"/>
<field name="message_type">notification</field>
@@ -21,19 +21,19 @@
<!-- 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.expense</field>
<field name="res_model">hr.expense</field>
<field name="default" eval="False"/>
<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.expense</field>
<field name="default" eval="False"/>
<field name="res_model">hr.expense</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.expense</field>
<field name="res_model">hr.expense</field>
<field name="description">Expense confirmed, waiting confirmation</field>
<field name="default" eval="True"/>
</record>
@@ -48,4 +48,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="hr.employee_mit" model="hr.employee">
<field name="address_home_id" ref="base.res_partner_address_5"/>
</record>
<record id="car_travel" model="product.product">
<field name="list_price">0.32</field>
<field name="standard_price">0.32</field>
<field name="type">service</field>
<field name="name">Car Travel Expenses</field>
<field name="default_code">CarTRA</field>
<field name="uom_id" ref="product.product_uom_km"/>
<field name="uom_po_id" ref="product.product_uom_km"/>
<field name="categ_id" ref="hr_expense.cat_expense"/>
<field name="can_be_expensed" eval="True" />
<field name="image" type="base64" file="hr_expense/static/img/car_travel-image.jpg"/>
</record>
<record id="air_ticket" model="product.product">
<field name="list_price">700.0</field>
<field name="standard_price">700.0</field>
<field name="type">service</field>
<field name="name">Air Flight</field>
<field name="default_code">AT</field>
<field name="categ_id" ref="hr_expense.cat_expense"/>
<field name="can_be_expensed" eval="True" />
<field name="image" type="base64" file="hr_expense/static/img/air_ticket-image.jpg"/>
</record>
<record id="hotel_rent" model="product.product">
<field name="list_price">400.0</field>
<field name="standard_price">400.0</field>
<field name="type">service</field>
<field name="name">Hotel Accommodation</field>
<field name="default_code">HA0</field>
<field name="uom_id" ref="product.product_uom_day"/>
<field name="uom_po_id" ref="product.product_uom_day"/>
<field name="categ_id" ref="hr_expense.cat_expense"/>
<field name="can_be_expensed" eval="True" />
<field name="image" type="base64" file="hr_expense/static/img/hotel_rent-image.jpg"/>
</record>
<record id="travel_by_air_expense" model="hr.expense">
<field name="name">Travel by Air</field>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="product_id" ref="air_ticket"/>
<field eval="700.0" name="unit_amount"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field eval="1.0" name="quantity"/>
</record>
<record id="hotel_bill_expense" model="hr.expense">
<field name="name">Hotel Expenses - Thymbra</field>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="hotel_rent"/>
<field eval="400.0" name="unit_amount"/>
<field name="product_uom_id" ref="product.product_uom_day"/>
<field eval="5.0" name="quantity"/>
</record>
</data>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record id="seq_hr_expense_invoice" model="ir.sequence">
@@ -10,4 +10,4 @@
</record>
</data>
</openerp>
</odoo>
+37
View File
@@ -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.expense_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 More to submit multiple expenses to your manager.</field>
<field name="action_id" ref="hr_expense.expense_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 about your expense or to attach a document</field>
<field name="action_id" ref="hr_expense.expense_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>
-3
View File
@@ -1,3 +0,0 @@
"id","name","ref","employee_id","line_ids/date_value","line_ids/name","line_ids/analytic_account","line_ids/unit_quantity","line_ids/ref","line_ids/unit_amount"
1,"September Expenses",09/06/09,"Fabien Pinckaers",05/09/06,"Travel by Car - Customer Seagate 2 - Double","Thymbra",130,,0.22
,,,,05/09/06,"Basic PC - Server for Seagate","Seagate P2",1,"S1234435",300
1 id name ref employee_id line_ids/date_value line_ids/name line_ids/analytic_account line_ids/unit_quantity line_ids/ref line_ids/unit_amount
2 1 September Expenses 09/06/09 Fabien Pinckaers 05/09/06 Travel by Car - Customer Seagate 2 - Double Thymbra 130 0.22
3 05/09/06 Basic PC - Server for Seagate Seagate P2 1 S1234435 300
-16
View File
@@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
from openerp import api, fields, models
class hr_department(models.Model):
_inherit = 'hr.department'
@api.multi
def _compute_expense_to_approve(self):
expense_data = self.env['hr.expense.expense'].read_group([('department_id', 'in', self.ids), ('state', '=', 'confirm')], ['department_id'], ['department_id'])
result = dict((data['department_id'][0], data['department_id_count']) for data in expense_data)
for department in self:
department.expense_to_approve_count = result.get(department.id, 0)
expense_to_approve_count = fields.Integer(
compute='_compute_expense_to_approve', string='Expenses to Approve')
-490
View File
@@ -1,490 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from openerp.osv import fields, osv
from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
from openerp.exceptions import UserError
def _employee_get(obj, cr, uid, context=None):
if context is None:
context = {}
ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
if ids:
return ids[0]
return False
class hr_expense_expense(osv.osv):
def _amount(self, cr, uid, ids, field_name, arg, context=None):
res= {}
for expense in self.browse(cr, uid, ids, context=context):
total = 0.0
for line in expense.line_ids:
total += line.unit_amount * line.unit_quantity
res[expense.id] = total
return res
def _get_expense_from_line(self, cr, uid, ids, context=None):
return [line.expense_id.id for line in self.pool.get('hr.expense.line').browse(cr, uid, ids, context=context)]
def _get_currency(self, cr, uid, context=None):
user = self.pool.get('res.users').browse(cr, uid, [uid], context=context)[0]
return user.company_id.currency_id.id
_name = "hr.expense.expense"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_description = "Expense"
_order = "id desc"
_columns = {
'name': fields.char('Description', required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'id': fields.integer('Sheet ID', readonly=True),
'date': fields.date('Date', select=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'journal_id': fields.many2one('account.journal', 'Force Journal', help = "The journal used when the expense is done."),
'employee_payable_account_id': fields.many2one('account.account', 'Employee Account', help="Employee payable account", domain=[('deprecated', '=', False)]),
'employee_id': fields.many2one('hr.employee', "Employee", required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'user_id': fields.many2one('res.users', 'User', required=True),
'date_confirm': fields.date('Confirmation Date', select=True, copy=False,
help="Date of the confirmation of the sheet expense. It's filled when the button Confirm is pressed."),
'date_valid': fields.date('Validation Date', select=True, copy=False,
help="Date of the acceptation of the sheet expense. It's filled when the button Accept is pressed."),
'user_valid': fields.many2one('res.users', 'Validation By', readonly=True, copy=False,
states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'account_move_id': fields.many2one('account.move', 'Ledger Posting', copy=False, track_visibility="onchange"),
'line_ids': fields.one2many('hr.expense.line', 'expense_id', 'Expense Lines', copy=True,
readonly=True, states={'draft':[('readonly',False)]} ),
'note': fields.text('Note'),
'amount': fields.function(_amount, string='Total Amount', digits=0,
store={
'hr.expense.line': (_get_expense_from_line, ['unit_amount','unit_quantity'], 10)
}),
'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'department_id':fields.many2one('hr.department','Department', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'company_id': fields.many2one('res.company', 'Company', required=True),
'state': fields.selection([
('draft', 'New'),
('cancelled', 'Refused'),
('confirm', 'Waiting Approval'),
('accepted', 'Approved'),
('done', 'Waiting Payment'),
('paid', 'Paid'),
],
'Status', readonly=True, track_visibility='onchange', copy=False,
help='When the expense request is created the status is \'Draft\'.\n It is confirmed by the user and request is sent to admin, the status is \'Waiting Confirmation\'.\
\nIf the admin accepts it, the status is \'Accepted\'.\n If the accounting entries are made for the expense request, the status is \'Waiting Payment\'.'),
}
_defaults = {
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.employee', context=c),
'date': fields.date.context_today,
'state': 'draft',
'employee_id': _employee_get,
'user_id': lambda cr, uid, id, c={}: id,
'currency_id': _get_currency,
}
def add_follower(self, cr, uid, ids, employee_id, context=None):
employee = self.pool.get('hr.employee').browse(cr, uid, employee_id, context=context)
if employee and employee.user_id:
self.message_subscribe_users(cr, uid, ids, user_ids=[employee.user_id.id], context=context)
def create(self, cr, uid, vals, context=None):
employee_id = vals.get('employee_id', False)
hr_expense_id = super(hr_expense_expense, self).create(cr, uid, vals, context=context)
self.add_follower(cr, uid, [hr_expense_id], employee_id, context=context)
return hr_expense_id
def write(self, cr, uid, ids, vals, context=None):
employee_id = vals.get('employee_id', False)
hr_expense_id = super(hr_expense_expense, self).write(cr, uid, ids, vals, context=context)
self.add_follower(cr, uid, ids, employee_id, context=context)
return hr_expense_id
def unlink(self, cr, uid, ids, context=None):
for rec in self.browse(cr, uid, ids, context=context):
if rec.state != 'draft':
raise UserError(_('You can only delete draft expenses!'))
return super(hr_expense_expense, self).unlink(cr, uid, ids, context)
def onchange_currency_id(self, cr, uid, ids, currency_id=False, company_id=False, context=None):
res = {'value': {'journal_id': False}}
journal_ids = self.pool.get('account.journal').search(cr, uid, [('type','=','purchase'), ('currency_id','=',currency_id), ('company_id', '=', company_id)], context=context)
if journal_ids:
res['value']['journal_id'] = journal_ids[0]
return res
def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
emp_obj = self.pool.get('hr.employee')
department_id = False
employee_payable_account_id = False
company_id = False
if employee_id:
employee = emp_obj.browse(cr, uid, employee_id, context=context)
department_id = employee.department_id.id
company_id = employee.company_id.id
if employee.address_home_id and employee.address_home_id.property_account_payable_id:
employee_payable_account_id = employee.address_home_id.property_account_payable_id.id
return {'value': {'department_id': department_id, 'company_id': company_id, 'employee_payable_account_id': employee_payable_account_id}}
def expense_confirm(self, cr, uid, ids, context=None):
for expense in self.browse(cr, uid, ids):
if not expense.line_ids:
raise UserError(_('You cannot submit expense which has no expense line.'))
if expense.employee_id and expense.employee_id.parent_id.user_id:
self.message_subscribe_users(cr, uid, [expense.id], user_ids=[expense.employee_id.parent_id.user_id.id])
return self.write(cr, uid, ids, {'state': 'confirm', 'date_confirm': time.strftime('%Y-%m-%d')}, context=context)
def expense_accept(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state': 'accepted', 'date_valid': time.strftime('%Y-%m-%d'), 'user_valid': uid}, context=context)
def expense_canceled(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
def _track_subtype(self, cr, uid, ids, init_values, context=None):
record = self.browse(cr, uid, ids[0], context=context)
if 'state' in init_values and record.state == 'accepted':
return 'hr_expense.mt_expense_approved'
elif 'state' in init_values and record.state == 'confirm':
return 'hr_expense.mt_expense_confirmed'
elif 'state' in init_values and record.state == 'cancelled':
return 'hr_expense.mt_expense_refused'
return super(hr_expense_expense, self)._track_subtype(cr, uid, ids, init_values, context=context)
def _notification_group_recipients(self, cr, uid, ids, message, recipients, done_ids, group_data, context=None):
""" Override because HR users / officers can approve / refuse expenses
directly form their notification email. """
group_hr_user = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'base.group_hr_user')
for recipient in recipients:
if recipient.id in done_ids:
continue
if recipient.user_ids and group_hr_user in recipient.user_ids[0].groups_id.ids:
group_data['group_hr_user'] |= recipient
done_ids.add(recipient.id)
return super(hr_expense_expense, self)._notification_group_recipients(cr, uid, ids, message, recipients, done_ids, group_data, context=context)
def _notification_get_recipient_groups(self, cr, uid, ids, message, recipients, context=None):
""" Override because HR users / officers can approve / refuse expenses
directly form their notification email. """
res = super(hr_expense_expense, self)._notification_get_recipient_groups(cr, uid, ids, message, recipients, context=context)
app_action = self._notification_link_helper(cr, uid, ids, 'workflow', context=context, signal='validate')
ref_action = self._notification_link_helper(cr, uid, ids, 'workflow', context=context, signal='refuse')
expense = self.browse(cr, uid, ids[0], context=context)
actions = []
if expense.state == 'confirm':
actions.append({'url': app_action, 'title': _('Approve')})
if expense.state in ['confirm', 'accepted']:
actions.append({'url': ref_action, 'title': _('Refuse')})
res['group_hr_user'] = {
'actions': actions
}
return res
def account_move_get(self, cr, uid, expense_id, context=None):
'''
This method prepare the creation of the account move related to the given expense.
:param expense_id: Id of expense for which we are creating account_move.
:return: mapping between fieldname and value of account move to create
:rtype: dict
'''
journal_obj = self.pool.get('account.journal')
expense = self.browse(cr, uid, expense_id, context=context)
company_id = expense.company_id.id
ref = expense.name
journal_id = False
if expense.journal_id:
journal_id = expense.journal_id.id
else:
journal_id = journal_obj.search(cr, uid, [('type', '=', 'purchase'), ('company_id', '=', company_id)])
if not journal_id:
raise UserError(_("No expense journal found. Please make sure you have a journal with type 'purchase' configured."))
journal_id = journal_id[0]
return {
'journal_id': journal_id,
'date': expense.date_confirm,
'ref': ref,
'company_id': company_id,
}
def line_get_convert(self, cr, uid, x, part, date, context=None):
partner_id = self.pool.get('res.partner')._find_accounting_partner(part).id
return {
'date_maturity': x.get('date_maturity', False),
'partner_id': partner_id,
'name': x['name'][:64],
'date': date,
'debit': x['price']>0 and x['price'],
'credit': x['price']<0 and -x['price'],
'account_id': x['account_id'],
'analytic_line_ids': x.get('analytic_line_ids', False),
'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
'currency_id': x.get('currency_id', False),
'tax_line_id': x.get('tax_line_id', False),
'ref': x.get('ref', False),
'quantity': x.get('quantity',1.00),
'product_id': x.get('product_id', False),
'product_uom_id': x.get('uom_id', False),
'analytic_account_id': x.get('account_analytic_id', False),
}
def compute_expense_totals(self, cr, uid, exp, company_currency, ref, account_move_lines, context=None):
'''
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
'''
cur_obj = self.pool.get('res.currency')
context = dict(context or {}, date=exp.date_confirm or time.strftime('%Y-%m-%d'))
total = 0.0
total_currency = 0.0
for i in account_move_lines:
if exp.currency_id.id != company_currency:
i['currency_id'] = exp.currency_id.id
i['amount_currency'] = i['price']
i['price'] = cur_obj.compute(cr, uid, exp.currency_id.id,
company_currency, i['price'],
context=context)
else:
i['amount_currency'] = False
i['currency_id'] = False
total -= i['price']
total_currency -= i['amount_currency'] or i['price']
return total, total_currency, account_move_lines
def action_move_create(self, cr, uid, ids, context=None):
'''
main function that is called when trying to create the accounting entries related to an expense
'''
move_obj = self.pool.get('account.move')
for exp in self.browse(cr, uid, ids, context=context):
if not exp.employee_payable_account_id:
raise UserError(_('No employee account payable found for the expense '))
company_currency = exp.company_id.currency_id.id
diff_currency_p = exp.currency_id.id != company_currency
#create the move that will contain the accounting entries
move_id = move_obj.create(cr, uid, self.account_move_get(cr, uid, exp.id, context=context), context=context)
#one account.move.line per expense line (+taxes..)
eml = self.move_line_get(cr, uid, exp.id, context=context)
#create one more move line, a counterline for the total on payable account
total, total_currency, eml = self.compute_expense_totals(cr, uid, exp, company_currency, exp.name, eml, context=context)
acc = exp.employee_payable_account_id.id or False
eml.append({
'type': 'dest',
'name': '/',
'price': total,
'account_id': acc,
'date_maturity': exp.date_confirm,
'amount_currency': diff_currency_p and total_currency or False,
'currency_id': diff_currency_p and exp.currency_id.id or False,
'ref': exp.name
})
#convert eml into an osv-valid format
lines = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, exp.employee_id.company_id.partner_id, exp.date_confirm, context=context)), eml)
journal_id = move_obj.browse(cr, uid, move_id, context).journal_id
# post the journal entry if 'Skip 'Draft' State for Manual Entries' is checked
move_obj.write(cr, uid, [move_id], {'line_ids': lines}, context=context)
self.write(cr, uid, ids, {'account_move_id': move_id, 'state': 'done'}, context=context)
move_obj.post(cr, uid, [move_id], context=context)
return True
def move_line_get(self, cr, uid, expense_id, context=None):
res = []
tax_obj = self.pool.get('account.tax')
if context is None:
context = {}
exp = self.browse(cr, uid, expense_id, context=context)
for line in exp.line_ids:
mres = self.move_line_get_item(cr, uid, line, context)
if not mres:
continue
res.append(mres)
# Use the default tax on product or account
taxes = []
if line.product_id:
fposition_id = False
fpos_obj = self.pool.get('account.fiscal.position')
fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
product = line.product_id
taxes = product.supplier_taxes_id
# If taxes are not related to the product, maybe they are in the account
if not taxes:
a = product.property_account_expense_id.id or product.categ_id.property_account_expense_categ_id.id
a = fpos_obj.map_account(cr, uid, fpos, a)
taxes = a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False
if not taxes:
continue
# Calculate tax lines and adjust base line
tax_ids = [tax.id for tax in taxes]
tax_res = tax_obj.browse(cr, uid, tax_ids, context=context).compute_all(line.unit_amount, exp.currency_id, line.unit_quantity, line.product_id, exp.user_id.partner_id)
res[-1]['price'] = tax_res['total_excluded']
res[-1]['tax_ids'] = tax_ids
for tax in tax_res['taxes']:
res.append({
'type': 'tax',
'name': tax['name'],
'price_unit': tax['amount'],
'quantity': 1,
'price': tax['amount'],
'account_id': tax['account_id'] or mres['account_id'],
'tax_line_id': tax['id'],
})
return res
def move_line_get_item(self, cr, uid, line, context=None):
company = line.expense_id.company_id
property_obj = self.pool.get('ir.property')
if line.product_id:
acc = line.product_id.property_account_expense_id
if not acc:
acc = line.product_id.categ_id.property_account_expense_categ_id
if not acc:
raise UserError(_('No purchase account found for the product %s (or for his category), please configure one.') % (line.product_id.name))
else:
acc = property_obj.get(cr, uid, 'property_account_expense_categ_id', 'product.category', context={'force_company': company.id})
if not acc:
raise UserError(_('Please configure Default Expense account for Product purchase: `property_account_expense_categ_id`.'))
return {
'type':'src',
'name': line.name.split('\n')[0][:64],
'price_unit':line.unit_amount,
'quantity':line.unit_quantity,
'price':line.total_amount,
'account_id':acc.id,
'product_id':line.product_id.id,
'uom_id':line.uom_id.id,
'account_analytic_id':line.analytic_account.id,
}
def action_view_move(self, cr, uid, ids, context=None):
'''
This function returns an action that display existing account.move of given expense ids.
'''
assert len(ids) == 1, 'This option should only be used for a single id at a time'
expense = self.browse(cr, uid, ids[0], context=context)
assert expense.account_move_id
try:
dummy, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'view_move_form')
except ValueError, e:
view_id = False
result = {
'name': _('Expense Account Move'),
'view_type': 'form',
'view_mode': 'form',
'view_id': view_id,
'res_model': 'account.move',
'type': 'ir.actions.act_window',
'target': 'current',
'res_id': expense.account_move_id.id,
}
return result
class product_template(osv.osv):
_inherit = "product.template"
_columns = {
'hr_expense_ok': fields.boolean('Can be Expensed', help="Specify if the product can be selected in an HR expense line."),
}
class hr_expense_line(osv.osv):
_name = "hr.expense.line"
_description = "Expense Line"
def _amount(self, cr, uid, ids, field_name, arg, context=None):
if not ids:
return {}
cr.execute("SELECT l.id,COALESCE(SUM(l.unit_amount*l.unit_quantity),0) AS amount FROM hr_expense_line l WHERE id IN %s GROUP BY l.id ",(tuple(ids),))
res = dict(cr.fetchall())
return res
def _get_uom_id(self, cr, uid, context=None):
result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'product', 'product_uom_unit')
return result and result[1] or False
_columns = {
'name': fields.char('Expense Note', required=True),
'date_value': fields.date('Date', required=True),
'expense_id': fields.many2one('hr.expense.expense', 'Expense', ondelete='cascade', select=True),
'total_amount': fields.function(_amount, string='Total', digits=0),
'unit_amount': fields.float('Unit Price', digits_compute=dp.get_precision('Product Price')),
'unit_quantity': fields.float('Quantities', digits_compute= dp.get_precision('Product Unit of Measure')),
'product_id': fields.many2one('product.product', 'Product', domain=[('hr_expense_ok','=',True)], required=True),
'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True),
'description': fields.text('Description'),
'analytic_account': fields.many2one('account.analytic.account','Analytic account'),
'ref': fields.char('Reference'),
'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of expense lines."),
}
_defaults = {
'unit_quantity': 1,
'date_value': lambda *a: time.strftime('%Y-%m-%d'),
'uom_id': _get_uom_id,
}
_order = "sequence, date_value desc"
def onchange_product_id(self, cr, uid, ids, product_id, context=None):
res = {}
if product_id:
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
res['name'] = product.name
amount_unit = product.price_get('standard_price')[product.id]
res['unit_amount'] = amount_unit
res['uom_id'] = product.uom_id.id
return {'value': res}
def onchange_uom(self, cr, uid, ids, product_id, uom_id, context=None):
res = {'value':{}}
if not uom_id or not product_id:
return res
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
uom = self.pool.get('product.uom').browse(cr, uid, uom_id, context=context)
if uom.category_id.id != product.uom_id.category_id.id:
res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')}
res['value'].update({'uom_id': product.uom_id.id})
return res
class account_move_line(osv.osv):
_inherit = "account.move.line"
def reconcile(self, cr, uid, ids, writeoff_acc_id=False, writeoff_journal_id=False, context=None):
res = super(account_move_line, self).reconcile(cr, uid, ids, writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id, context=context)
#when making a full reconciliation of account move lines 'ids', we may need to recompute the state of some hr.expense
account_move_ids = [aml.move_id.id for aml in self.browse(cr, uid, ids, context=context)]
expense_obj = self.pool.get('hr.expense.expense')
currency_obj = self.pool.get('res.currency')
if account_move_ids:
expense_ids = expense_obj.search(cr, uid, [('account_move_id', 'in', account_move_ids)], context=context)
for expense in expense_obj.browse(cr, uid, expense_ids, context=context):
if expense.state == 'done':
#making the postulate it has to be set paid, then trying to invalidate it
new_status_is_paid = True
for aml in expense.account_move_id.line_ids:
if aml.account_id.internal_type == 'payable' and not currency_obj.is_zero(cr, uid, expense.company_id.currency_id, aml.amount_residual):
new_status_is_paid = False
if new_status_is_paid:
expense_obj.write(cr, uid, [expense.id], {'state': 'paid'}, context=context)
return res
-101
View File
@@ -1,101 +0,0 @@
<?xml version="1.0" ?>
<openerp>
<data noupdate="1">
<record id="hr.employee_niv" model="hr.employee">
<field name="address_home_id" ref="base.res_partner_address_4"/>
</record>
<record id="hr.employee_mit" model="hr.employee">
<field name="address_home_id" ref="base.res_partner_address_5"/>
</record>
<record id="car_travel" model="product.product">
<field name="list_price">0.32</field>
<field name="type">service</field>
<field name="name">Car Travel Expenses</field>
<field name="default_code">CarTRA</field>
<field name="uom_id" ref="product.product_uom_km"/>
<field name="uom_po_id" ref="product.product_uom_km"/>
<field name="categ_id" ref="hr_expense.cat_expense"/>
<field name="hr_expense_ok" eval="True" />
<field name="image" type="base64" file="hr_expense/static/img/car_travel-image.jpg"/>
</record>
<record id="air_ticket" model="product.product">
<field name="list_price">700.0</field>
<field name="standard_price">700.0</field>
<field name="type">service</field>
<field name="name">Air Flight</field>
<field name="default_code">AT</field>
<field name="categ_id" ref="hr_expense.cat_expense"/>
<field name="hr_expense_ok" eval="True" />
<field name="image" type="base64" file="hr_expense/static/img/air_ticket-image.jpg"/>
</record>
<record id="hotel_rent" model="product.product">
<field name="list_price">400.0</field>
<field name="type">service</field>
<field name="name">Hotel Accommodation</field>
<field name="default_code">HA0</field>
<field name="categ_id" ref="hr_expense.cat_expense"/>
<field name="hr_expense_ok" eval="True" />
<field name="image" type="base64" file="hr_expense/static/img/hotel_rent-image.jpg"/>
</record>
<record id="expenses0" model="hr.expense.expense">
<field name="name">Expenses</field>
<field name="date" eval="time.strftime('%Y-08-25')"/>
<field name="employee_id" ref="hr.employee_niv"/>
</record>
<record id="expenses_line0" model="hr.expense.line">
<field name="name">Travel Expenses</field>
<field name="date_value" eval="time.strftime('%Y-08-10')"/>
<field name="analytic_account" ref="analytic.analytic_our_super_product"/>
<field name="product_id" ref="hotel_rent"/>
<field name="expense_id" ref="expenses0"/>
<field eval="200.0" name="unit_amount"/>
<field name="uom_id" ref="product.product_uom_unit"/>
<field eval="1.0" name="unit_quantity"/>
</record>
<record id="sep_expenses" model="hr.expense.expense">
<field name="employee_id" ref="hr.employee_mit"/>
<field name="name">September Expenses</field>
<field name="date" eval="time.strftime('%Y-09-28')"/>
</record>
<record id="travel_by_air_sep" model="hr.expense.line">
<field name="name">Travel by Air</field>
<field name="date_value" eval="time.strftime('%Y-09-03')"/>
<field name="analytic_account" ref="analytic.analytic_our_super_product"/>
<field name="product_id" ref="air_ticket"/>
<field name="expense_id" ref="sep_expenses"/>
<field eval="700.0" name="unit_amount"/>
<field name="uom_id" ref="product.product_uom_unit"/>
<field eval="1.0" name="unit_quantity"/>
</record>
<record id="hotel_bill_sep" model="hr.expense.line">
<field name="name">Hotel Expenses - Thymbra</field>
<field name="date_value" eval="time.strftime('%Y-09-20')"/>
<field name="analytic_account" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="hotel_rent"/>
<field name="expense_id" ref="sep_expenses"/>
<field eval="400.0" name="unit_amount"/>
<field name="uom_id" ref="product.product_uom_unit"/>
<field eval="5.0" name="unit_quantity"/>
</record>
<record id="car_travel_sep" model="hr.expense.line">
<field name="name">Bruxelles - Paris</field>
<field name="date_value" eval="time.strftime('%Y-09-15')"/>
<field name="analytic_account" ref="analytic.analytic_nebula"/>
<field name="product_id" ref="car_travel"/>
<field name="expense_id" ref="sep_expenses"/>
<field eval="0.30" name="unit_amount"/>
<field name="uom_id" ref="product.product_uom_km"/>
<field eval="622.0" name="unit_quantity"/>
</record>
</data>
</openerp>
@@ -1,30 +0,0 @@
<openerp>
<data>
<record id="product_expense_installer_tree_view" model="ir.ui.view">
<field name="name">product.product.tree</field>
<field name="model">product.product</field>
<field eval="37" name="priority"/>
<field name="arch" type="xml">
<tree editable="bottom" string="Products">
<field name="name"/>
<field name="standard_price"/>
<field name="list_price"/>
<field name="uom_id" string="Unit of Measure"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="product_normal_form_view_installer">
<field name="name">Review Your Expenses Products</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.product</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context" eval="'{\'default_type\':\'service\',\'default_purchase_ok\':True, \'default_sale_ok\':False, \'default_hr_expense_ok\':True,\'default_categ_id\': ' + str(ref('cat_expense')) +'}'"/>
<field name="domain">[('hr_expense_ok','=',True)]</field>
<field name="view_id" ref="product_expense_installer_tree_view"/>
<field name="help">Define one product for each expense type allowed for an employee (travel by car, hostel, restaurant, etc). If you reimburse the employees at a fixed rate, set a cost and a unit of measure on the product. If you reimburse based on real costs, set the cost at 0.00. The user will set the real price when recording his expense sheet.</field>
</record>
</data>
</openerp>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<report
id="action_report_hr_expense"
string="HR Expense"
model="hr.expense.expense"
report_type="qweb-pdf"
name="hr_expense.report_expense"
file="hr_expense.report_expense"
/>
</data>
</openerp>
-290
View File
@@ -1,290 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Expenses -->
<record id="view_expenses_line_tree" model="ir.ui.view">
<field name="name">hr.expense.line.tree</field>
<field name="model">hr.expense.line</field>
<field name="arch" type="xml">
<tree string="Expense Lines">
<field name="sequence" invisible="1"/>
<field name="date_value"/>
<field name="name"/>
<field name="ref"/>
<field name="analytic_account"/>
<field name="unit_amount"/>
<field name="unit_quantity"/>
<field name="total_amount" sum="Total" widget="monetary"/>
</tree>
</field>
</record>
<record id="view_expenses_tree" model="ir.ui.view">
<field name="name">hr.expense.expense.tree</field>
<field name="model">hr.expense.expense</field>
<field name="arch" type="xml">
<tree string="Expenses" decoration-warning="state=='draft'" decoration-bf="message_needaction == True">
<field name="employee_id"/>
<field name="department_id" invisible="1"/>
<field name="date"/>
<field name="user_id" invisible="1"/>
<field name="name" string="Expense Sheet"/>
<field name="amount" sum="Total Amount" widget="monetary"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="state"/>
<field name="message_needaction" invisible="1"/>
</tree>
</field>
</record>
<record id="view_editable_expenses_tree" model="ir.ui.view">
<field name="name">hr.expense.expense.tree</field>
<field name="model">hr.expense.expense</field>
<field name="arch" type="xml">
<tree decoration-warning="state == 'draft'" decoration-muted="state == 'cancelled'" string="Expenses" editable="top">
<field name="employee_id"/>
<field name="date"/>
<field name="department_id"/>
<field name="name" string="Expense Sheet"/>
<field name="amount" widget="monetary"/>
<field name="state"/>
<button name="confirm" states="draft" string="Confirm" type="workflow" icon="gtk-apply"/>
<button name="refuse" states="confirm,draft,accepted" string="Refuse" type="workflow" icon="gtk-no" groups="base.group_hr_user"/>
</tree>
</field>
</record>
<record id="view_expenses_form" model="ir.ui.view">
<field name="name">hr.expense.form</field>
<field name="model">hr.expense.expense</field>
<field eval="25" name="priority"/>
<field name="arch" type="xml">
<form string="Expenses Sheet">
<header>
<button name="confirm" states="draft" string="Submit to Manager" type="workflow" class="oe_highlight"/>
<button name="validate" states="confirm" string="Approve" type="workflow" groups="base.group_hr_user" class="oe_highlight"/>
<button name="refuse" states="confirm,accepted" string="Refuse" type="workflow" groups="base.group_hr_user" />
<button name="draft" states="confirm,cancelled" string="Set to Draft" type="workflow" groups="base.group_hr_user" />
<button name="done" states="accepted" string="Generate Accounting Entries" type="workflow" groups="account.group_account_invoice,account.group_account_user" class="oe_highlight"/>
<button name="action_view_move" states="done" string="Open Accounting Entries" type="object" groups="account.group_account_invoice,account.group_account_user"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,accepted,done,paid" statusbar_colors='{"confirm":"blue","cancelled":"red"}'/>
</header>
<sheet>
<group>
<group>
<field name="employee_id" on_change="onchange_employee_id(employee_id)"/>
<field name="date"/>
<field name="department_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="name" string="Expense Sheet" placeholder="e.g. Business travel at Chicago"/>
<field name="user_valid" attrs="{'invisible': [('state','=','draft')]}" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_hr_user']}"/>
<field name="currency_id" groups="base.group_multi_currency" on_change="onchange_currency_id(currency_id, company_id)"/>
</group>
</group>
<notebook>
<page string="Expense Lines">
<field name="line_ids" context="{'currency_id': currency_id, 'default_analytic_account': context.get('analytic_account', '')}">
<form string="Expense Lines">
<group>
<group>
<field name="product_id" on_change="onchange_product_id(product_id, context)" context="{'default_hr_expense_ok':1}"/>
<field name="name"/>
<field name="ref"/>
<field name="analytic_account"/>
</group>
<group>
<field name="unit_amount"/>
<label for="unit_quantity"/>
<div>
<field name="unit_quantity" class="oe_inline"/>
<field name="uom_id" on_change="onchange_uom(product_id, uom_id, context)" class="oe_inline"/>
</div>
<field name="date_value" />
</group>
</group>
</form>
<tree string="Expense Lines" editable="bottom">
<field name="sequence" invisible="1"/>
<field name="product_id" on_change="onchange_product_id(product_id, context)" context="{'default_hr_expense_ok':1}"/>
<field name="date_value" string="Expense Date"/>
<field name="name" string="Description"/>
<field name="ref"/>
<field name="analytic_account"/>
<field name="uom_id" on_change="onchange_uom(product_id, uom_id, context)"/>
<field name="unit_amount"/>
<field name="unit_quantity"/>
<field name="total_amount" sum="Total" widget="monetary"/>
</tree>
</field>
<group>
<div>
<separator string="Description"/>
<field name="note" placeholder="Free Notes"/>
</div>
<group class="oe_subtotal_footer oe_right">
<field name="amount" widget="monetary" options="{'currency_field': 'currency_id'}" class="oe_subtotal_footer_separator"/>
</group>
</group>
</page>
<page string="Accounting Info" groups="account.group_account_user" attrs="{'invisible': [('state', 'not in', ['done', 'paid', 'accepted'])]}">
<group>
<group>
<field name="journal_id" widget="selection" attrs="{'readonly':[('state','=','done')]}" domain="[('type', '=', 'purchase')]" string="Journal"/>
</group>
<group>
<field name="employee_payable_account_id" widget="selection" attrs="{'readonly':[('state','=','done')]}" domain="[('internal_type', '=', 'payable')]"/>
</group>
</group>
</page>
</notebook>
</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_filter" model="ir.ui.view">
<field name="name">hr.expense.expense.filter</field>
<field name="model">hr.expense.expense</field>
<field name="arch" type="xml">
<search string="Expense">
<field name="name" string="Expenses"/>
<field name="date"/>
<field name="employee_id"/>
<field name="department_id" string="Department" context="{'invisible_department': False}"/>
<filter domain="[('state','=','draft')]" string="New" help="New Expense"/>
<filter domain="[('state','=','confirm')]" string="To Approve" name="confirm" help="Confirmed Expenses"/>
<filter domain="[('state','=','accepted')]" string="To Pay" name="approved" help="Expenses to Invoice"/>
<separator />
<filter string="My Expenses" domain="[('employee_id.user_id', '=', uid)]"/>
<filter string="My Team Expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="base.group_hr_manager" help="Expenses of Your Team Member"/>
<separator />
<filter string="Important Messages" name="message_needaction" domain="[('message_needaction','=',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'}"/>
<filter string="Expenses Month" domain="[]" context="{'group_by':'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="expense_all" model="ir.actions.act_window">
<field name="name">Expenses</field>
<field name="res_model">hr.expense.expense</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="view_id" ref="view_expenses_tree"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to register new expenses.
</p><p>
Odoo will ensure the whole process is followed; the expense
sheet is validated by manager(s), the employee is reimbursed
from his expenses, some expenses must be re-invoiced to the
customers.
</p>
</field>
</record>
<record id="view_product_hr_expense_form" model="ir.ui.view">
<field name="name">product.template.expense.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<div name="options" position="inside">
<div>
<field name="hr_expense_ok"/>
<label for="hr_expense_ok"/>
</div>
</div>
</field>
</record>
<record id="hr_expense_product" model="ir.actions.act_window">
<field name="name">Expense Products</field>
<field name="res_model">product.product</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{"default_hr_expense_ok":1}</field>
<field name="domain">[('hr_expense_ok','=',True)]</field>
<field name="search_view_id" ref="product.product_search_form_view"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new expense category.
</p>
</field>
</record>
<record id="action_approved_expense" model="ir.actions.act_window">
<field name="name">Expenses</field>
<field name="res_model">hr.expense.expense</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_expenses_tree"/>
<field name="domain">[('state','=','accepted')]</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to register new expenses.
</p><p>
Odoo will ensure the whole process is followed; the expense
sheet is validated by manager(s), the employee is reimbursed
from his expenses, some expenses must be re-invoiced to the
customers.
</p>
</field>
</record>
<record id="action_request_approve_expense" model="ir.actions.act_window">
<field name="name">Expenses to Approve</field>
<field name="res_model">hr.expense.expense</field>
<field name="view_type">form</field>
<field name="context">{'search_default_confirm':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="help" type="html">
<p class="oe_view_nocontent_create">
Click to register new expenses.
</p><p>
Odoo will ensure the whole process is followed; the expense
sheet is validated by manager(s), the employee is reimbursed
from his expenses, some expenses must be re-invoiced to the
customers.
</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" groups="base.group_configuration,base.group_hr_manager" action="hr_expense_product"/>
<menuitem action="expense_all" id="menu_expense_all" name="Expenses" parent="menu_hr_expense_root"/>
<menuitem action="action_request_approve_expense" id="menu_expense_to_approve" name="Approvals"
parent="menu_hr_expense_root" groups="base.group_hr_user"/>
<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.expense</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="context">{
'search_default_confirm': 1,
'search_default_department_id': [active_id],
'default_department_id': active_id}
</field>
</record>
</data>
</openerp>
-97
View File
@@ -1,97 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Workflow definition -->
<record id="wkf_expenses" model="workflow">
<field name="name">hr.wkf.expenses</field>
<field name="osv">hr.expense.expense</field>
<field name="on_create">True</field>
</record>
<record id="act_draft" model="workflow.activity">
<field name="wkf_id" ref="wkf_expenses"/>
<field name="name">draft</field>
<field name="flow_start">True</field>
<field name="kind">function</field>
<field name="action">write({'state': 'draft'})</field>
</record>
<record id="act_confirm" model="workflow.activity">
<field name="wkf_id" ref="wkf_expenses"/>
<field name="name">confirm</field>
<field name="kind">function</field>
<field name="action">expense_confirm()</field>
</record>
<record id="act_accepted" model="workflow.activity">
<field name="wkf_id" ref="wkf_expenses"/>
<field name="name">accepted</field>
<field name="kind">function</field>
<field name="action">expense_accept()</field>
</record>
<record id="act_refused" model="workflow.activity">
<field name="wkf_id" ref="wkf_expenses"/>
<field name="name">refused</field>
<field name="kind">function</field>
<field name="action">expense_canceled()</field>
</record>
<record id="act_done" model="workflow.activity">
<field name="wkf_id" ref="wkf_expenses"/>
<field name="name">done</field>
<field name="kind">function</field>
<field name="action">action_move_create()</field>
</record>
<record id="t1" model="workflow.transition">
<field name="act_from" ref="act_draft"/>
<field name="act_to" ref="act_confirm"/>
<field name="signal">confirm</field>
</record>
<record id="t2" model="workflow.transition">
<field name="act_from" ref="act_confirm"/>
<field name="act_to" ref="act_accepted"/>
<field name="signal">validate</field>
<field name="group_id" ref="base.group_hr_user"/>
</record>
<record id="t4" model="workflow.transition">
<field name="act_from" ref="act_confirm"/>
<field name="act_to" ref="act_refused"/>
<field name="signal">refuse</field>
<field name="group_id" ref="base.group_hr_user"/>
</record>
<record id="t6" model="workflow.transition">
<field name="act_from" ref="act_accepted"/>
<field name="act_to" ref="act_refused"/>
<field name="signal">refuse</field>
<field name="group_id" ref="base.group_hr_user"/>
</record>
<record id="t7" model="workflow.transition">
<field name="act_from" ref="act_confirm"/>
<field name="act_to" ref="act_draft"/>
<field name="signal">draft</field>
<field name="group_id" ref="base.group_hr_user"/>
</record>
<record id="t8" model="workflow.transition">
<field name="act_from" ref="act_accepted"/>
<field name="act_to" ref="act_done"/>
<field name="signal">done</field>
</record>
<record id="t10" model="workflow.transition">
<field name="act_from" ref="act_refused"/>
<field name="act_to" ref="act_draft"/>
<field name="signal">draft</field>
<field name="group_id" ref="base.group_hr_user"/>
</record>
</data>
</openerp>
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
import account_move_line
import hr_department
import hr_expense
import product_template
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import api, models
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
@api.multi
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
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 l.move_id.matched_percentage == 1]
if account_move_ids:
expenses = self.env['hr.expense'].search([('account_move_id', 'in', account_move_ids)])
expenses.paid_expenses()
return res
+16
View File
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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)
for department in self:
department.expense_to_approve_count = result.get(department.id, 0)
expense_to_approve_count = fields.Integer(compute='_compute_expense_to_approve', string='Expenses to Approve')
+295
View File
@@ -0,0 +1,295 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import api, fields, models, _
from openerp.exceptions import UserError
import openerp.addons.decimal_precision as dp
class HrExpense(models.Model):
_name = "hr.expense"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_description = "Expense"
_order = "date"
name = fields.Char(string='Description', readonly=True, required=True, states={'draft': [('readonly', False)]})
date = fields.Date(readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today)
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)])
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'))
unit_amount = fields.Float(string='Unit Price', readonly=True, required=True, states={'draft': [('readonly', False)]}, digits=dp.get_precision('Product Price'))
quantity = fields.Float(required=True, readonly=True, states={'draft': [('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'))
total_amount = fields.Float(string='Total', store=True, compute='_compute_amount', digits=dp.get_precision('Account'))
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id)
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='Contract', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, oldname='analytic_account')
department_id = fields.Many2one('hr.department', string='Department', states={'post': [('readonly', True)], 'done': [('readonly', True)]})
description = fields.Text()
payment_mode = fields.Selection([("own_account", "By Employee's payment method"), ("company_account", "By Company's payment method")], default='own_account', states={'done': [('readonly', True)], 'post': [('readonly', True)]})
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), required=True, 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\'.')
@api.depends('quantity', 'unit_amount', 'tax_ids', 'currency_id')
def _compute_amount(self):
for expense in self:
expense.untaxed_amount = expense.unit_amount * expense.quantity
taxes = expense.tax_ids.compute_all(expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id, expense.employee_id.user_id.partner_id)
expense.total_amount = taxes.get('total_included')
@api.multi
def _compute_attachment_number(self):
attachment_data = self.env['ir.attachment'].read_group([('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)], ['res_id'], ['res_id'])
attachment = dict((data['res_id'], data['res_id_count']) for data in attachment_data)
for expense in self:
expense.attachment_number = attachment.get(expense.id, 0)
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
self.name = '%s' % (self.product_id.display_name or '')
self.unit_amount = self.env['product.template']._price_get(self.product_id, 'standard_price')[self.product_id.id]
self.product_uom_id = self.product_id.uom_id
@api.onchange('product_uom_id')
def _onchange_product_uom_id(self):
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()
@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)
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],
'date': self.date,
'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'),
'ref': line.get('ref'),
'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'),
}
@api.multi
def _compute_expense_totals(self, company_currency, account_move_lines):
'''
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
'''
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=self.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
@api.multi
def action_move_create(self):
'''
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."))
#create the move that will contain the accounting entries
move = self.env['account.move'].create({
'journal_id': self.journal_id.id,
'company_id': self.env.user.company_id.id,
})
for expense in self:
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
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines)
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
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
move_lines.append({
'type': 'dest',
'name': '/',
'price': total,
'account_id': emp_account,
'date_maturity': expense.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)
move.write({'line_ids': lines})
expense.write({'account_move_id': move.id, 'state': 'post'})
if expense.payment_mode == 'company_account':
expense.paid_expenses()
return move.post()
@api.multi
def _move_line_get(self):
account_move = []
for expense in self:
if expense.product_id:
account = expense.product_id.product_tmpl_id._get_product_accounts()['expense']
if not account:
raise UserError(_("No Expense account found for the product %s (or for it's category), please configure one.") % (expense.product_id.name))
else:
account = self.env['ir.property'].with_context(force_company=expense.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`.'))
move_line = {
'type': 'src',
'name': expense.name.split('\n')[0][:64],
'price_unit': expense.unit_amount,
'quantity': expense.quantity,
'price': expense.total_amount,
'account_id': account.id,
'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
taxes = expense.tax_ids.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'] = expense.tax_ids.id
for tax in taxes['taxes']:
account_move.append({
'type': 'tax',
'name': tax['name'],
'price_unit': tax['amount'],
'quantity': 1,
'price': tax['amount'],
'account_id': tax['account_id'] or move_line['account_id'],
'tax_line_id': tax['id'],
})
return account_move
@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
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
can_be_expensed = fields.Boolean(help="Specify whether the product can be selected in an HR expense.")
-1
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hr_expense_report
+57 -76
View File
@@ -1,92 +1,73 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import tools
from openerp.osv import fields, osv
from openerp import fields, models, tools
from openerp.addons.decimal_precision import decimal_precision as dp
import openerp.addons.decimal_precision as dp
class hr_expense_report(osv.osv):
class HrExpenseReport(models.Model):
_name = "hr.expense.report"
_description = "Expenses Statistics"
_auto = False
_rec_name = 'date'
_columns = {
'date': fields.date('Date ', readonly=True),
'create_date': fields.datetime('Creation Date', readonly=True),
'product_id':fields.many2one('product.product', 'Product', readonly=True),
'journal_id': fields.many2one('account.journal', 'Force Journal', readonly=True),
'product_qty':fields.float('Product Quantity', readonly=True),
'employee_id': fields.many2one('hr.employee', "Employee's Name", readonly=True),
'date_confirm': fields.date('Confirmation Date', readonly=True),
'date_valid': fields.date('Validation Date', readonly=True),
'department_id':fields.many2one('hr.department','Department', readonly=True),
'company_id':fields.many2one('res.company', 'Company', readonly=True),
'user_id':fields.many2one('res.users', 'Validation User', readonly=True),
'currency_id': fields.many2one('res.currency', 'Currency', readonly=True),
'price_total':fields.float('Total Price', readonly=True, digits=0),
'delay_valid':fields.float('Delay to Valid', readonly=True),
'delay_confirm':fields.float('Delay to Confirm', readonly=True),
'analytic_account': fields.many2one('account.analytic.account','Analytic account',readonly=True),
'price_average':fields.float('Average Price', readonly=True, digits=0),
'nbr':fields.integer('# of Lines', readonly=True), # TDE FIXME master: rename into nbr_lines
'no_of_products':fields.integer('# of Products', readonly=True),
'no_of_account':fields.integer('# of Accounts', readonly=True),
'state': fields.selection([
('draft', 'Draft'),
('confirm', 'Waiting confirmation'),
('accepted', 'Accepted'),
('done', 'Done'),
('cancelled', 'Cancelled')],
'Status', readonly=True),
}
date = fields.Date(readonly=True)
create_date = fields.Datetime(string='Creation Date', readonly=True)
product_id = fields.Many2one('product.product', string='Product', readonly=True)
journal_id = fields.Many2one('account.journal', string='Force Journal', readonly=True)
product_qty = fields.Float(string='Total Quantity', readonly=True)
employee_id = fields.Many2one('hr.employee', string="Employee's Name", readonly=True)
department_id = fields.Many2one('hr.department', string='Department', readonly=True)
company_id = fields.Many2one('res.company', string='Company', readonly=True)
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True)
price_total = fields.Float(string='Total Price', readonly=True, digits=0)
analytic_account_id = fields.Many2one('account.analytic.account', string='Contract', readonly=True, oldname='analytic_account')
price_average = fields.Float(string='Average Price', readonly=True, digits=0)
nbr = fields.Integer(string='# of Expenses', readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('submit', 'Submitted'),
('approve', 'Approved'),
('post', 'Waiting Payment'),
('done', 'Paid'),
('cancel', 'Refused')], readonly=True)
_order = 'date desc'
def init(self, cr):
tools.drop_view_if_exists(cr, 'hr_expense_report')
cr.execute("""
create or replace view hr_expense_report as (
select
min(l.id) as id,
s.date as date,
s.create_date as create_date,
s.employee_id,
s.journal_id,
s.currency_id,
s.date_confirm as date_confirm,
s.date_valid as date_valid,
s.user_valid as user_id,
s.department_id,
avg(extract('epoch' from age(s.date_valid,s.date)))/(3600*24) as delay_valid,
avg(extract('epoch' from age(s.date_valid,s.date_confirm)))/(3600*24) as delay_confirm,
l.product_id as product_id,
l.analytic_account as analytic_account,
sum(l.unit_quantity * u.factor) as product_qty,
s.company_id as company_id,
sum(l.unit_quantity*l.unit_amount) as price_total,
(sum(l.unit_quantity*l.unit_amount)/sum(case when l.unit_quantity=0 or u.factor=0 then 1 else l.unit_quantity * u.factor end))::decimal(16,2) as price_average,
count(*) as nbr,
(select unit_quantity from hr_expense_line where id=l.id and product_id is not null) as no_of_products,
(select analytic_account from hr_expense_line where id=l.id and analytic_account is not null) as no_of_account,
s.state
from hr_expense_line l
left join hr_expense_expense s on (s.id=l.expense_id)
left join product_uom u on (u.id=l.uom_id)
group by
s.date,
s.create_date,
s.date_confirm,
s.date_valid,
l.product_id,
l.analytic_account,
s.currency_id,
s.user_valid,
s.department_id,
l.uom_id,
l.id,
s.state,
s.journal_id,
s.company_id,
s.employee_id
CREATE OR REPLACE VIEW hr_expense_report AS (
SELECT
MIN(e.id) AS id,
e.date AS date,
e.create_date AS create_date,
e.employee_id,
e.journal_id,
e.currency_id,
e.department_id,
e.product_id AS product_id,
e.analytic_account_id AS analytic_account_id,
SUM(e.quantity * u.factor) AS product_qty,
e.company_id AS company_id,
SUM(e.quantity * e.unit_amount) AS price_total,
(SUM(e.quantity * e.unit_amount) / SUM(CASE WHEN e.quantity=0 OR u.factor=0 THEN 1 ELSE e.quantity * u.factor END))::DECIMAL(16,2) AS price_average,
COUNT(*) AS nbr,
e.state
FROM hr_expense e
LEFT JOIN product_uom u ON (u.id=e.product_uom_id)
GROUP BY
e.date,
e.create_date,
e.product_id,
e.analytic_account_id,
e.currency_id,
e.department_id,
e.product_uom_id,
e.id,
e.state,
e.journal_id,
e.company_id,
e.employee_id
)
""")
@@ -1,70 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<record id="view_hr_expense_report_pivot" model="ir.ui.view">
<field name="name">hr.expense.report.pivot</field>
<field name="model">hr.expense.report</field>
<field name="arch" type="xml">
<pivot string="Expenses Analysis" disable_linking="True">
<field name="employee_id" type="row"/>
<field name="create_date" interval="month" type="col"/>
<field name="price_total" type="measure"/>
<field name="price_average" type="measure"/>
</pivot>
</field>
<record id="hr_expense_report_view_pivot" model="ir.ui.view">
<field name="name">hr.expense.report.pivot</field>
<field name="model">hr.expense.report</field>
<field name="arch" type="xml">
<pivot string="Expenses Analysis" disable_linking="True">
<field name="employee_id" type="row"/>
<field name="create_date" interval="month" type="col"/>
<field name="price_total" type="measure"/>
<field name="price_average" type="measure"/>
</pivot>
</field>
</record>
<record id="view_hr_expense_report_graph" model="ir.ui.view">
<field name="name">hr.expense.report.graph</field>
<field name="model">hr.expense.report</field>
<field name="arch" type="xml">
<graph string="Expenses Analysis">
<field name="employee_id" type="row"/>
<field name="create_date" interval="month" type="col"/>
<field name="price_total" type="measure"/>
</graph>
</field>
<record id="hr_expense_report_view_graph" model="ir.ui.view">
<field name="name">hr.expense.report.graph</field>
<field name="model">hr.expense.report</field>
<field name="arch" type="xml">
<graph string="Expenses Analysis">
<field name="employee_id" type="row"/>
<field name="create_date" interval="month" type="col"/>
<field name="price_total" type="measure"/>
</graph>
</field>
</record>
<record id="view_hr_expense_report_search" model="ir.ui.view">
<record id="hr_expense_report_view_search" model="ir.ui.view">
<field name="name">hr.expense.report.search</field>
<field name="model">hr.expense.report</field>
<field name="arch" type="xml">
<search string="Expenses Analysis">
<filter string="Waiting" domain="[('state', '=' ,'confirm')]" help="Confirm Expenses"/>
<filter string="Approved" domain="[('state','=','accepted')]" help="Approved Expenses"/>
<filter string="Done" domain="[('state','=', 'done')]" help="Done Expenses"/>
<filter string="Submitted" domain="[('state', '=', 'submit')]" help="Submitted Expenses"/>
<filter string="Approved" domain="[('state', '=', 'approve')]" help="Approved Expenses"/>
<filter string="Post" domain="[('state', '=', 'post')]" help="posted Expenses"/>
<filter string="Paid" domain="[('state', '=', 'done')]" help="Paid Expenses"/>
<field name="employee_id"/>
<field name="department_id"/>
<field name="analytic_account_id"/>
<group expand="0" string="Extended Filters...">
<field name="product_id"/>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="date" string="Creation Date"/>
<field name="date_confirm"/>
<field name="date_valid"/>
</group>
<group expand="1" string="Group By">
<filter string="Validation User" name="User" context="{'group_by':'user_id'}"/>
<filter string="Department" context="{'group_by':'department_id'}"/>
<filter string="Product" context="{'group_by':'product_id'}"/>
<filter string="Status" context="{'group_by':'state'}"/>
<filter string="Company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<filter string="Employee" name="Employee" context="{'group_by':'employee_id'}"/>
<filter string="Department" context="{'group_by': 'department_id'}"/>
<filter string="Product" context="{'group_by': 'product_id'}"/>
<filter string="Status" context="{'group_by': 'state'}"/>
<filter string="Contract" context="{'group_by': 'analytic_account_id'}"/>
<filter string="Company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
<separator/>
<filter string="Create Month" context="{'group_by':'create_date:month'}" help="Creation Date"/>
<filter string="Date" context="{'group_by': 'date:day'}" help="Date"/>
</group>
</search>
</field>
</record>
<record id="action_hr_expense_report_all" model="ir.actions.act_window">
<record id="hr_expense_report_action" model="ir.actions.act_window">
<field name="name">Expenses Analysis</field>
<field name="res_model">hr.expense.report</field>
<field name="view_type">form</field>
<field name="view_mode">pivot,graph</field>
<field name="context">{'search_default_year':1,'search_default_month':1,'search_default_employee':1}</field>
<field name="search_view_id" ref="view_hr_expense_report_search"/>
<field name="context">{'search_default_year':1, 'search_default_month':1, 'search_default_employee':1}</field>
<field name="search_view_id" ref="hr_expense_report_view_search"/>
</record>
<record id="action_hr_expense_report_filtered" model="ir.actions.act_window">
@@ -73,16 +72,15 @@
<field name="view_type">form</field>
<field name="view_mode">pivot,graph</field>
<field name="context">{
'search_default_year':1,
'search_default_month':1,
'search_default_employee':1,
'search_default_year': 1,
'search_default_month': 1,
'search_default_employee': 1,
'search_default_department_id': [active_id],
'default_department_id': active_id}
</field>
<field name="search_view_id" ref="view_hr_expense_report_search"/>
<field name="search_view_id" ref="hr_expense_report_view_search"/>
</record>
<menuitem name="Reports" id="menu_hr_expense_report" sequence="99" parent="menu_hr_expense_root" action="action_hr_expense_report_all"/>
<menuitem id="menu_hr_expense_report" name="Reports" sequence="99" parent="menu_hr_expense_root" action="hr_expense_report_action"/>
</data>
</openerp>
</odoo>
@@ -1,8 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_expense_expense_user,hr.expense.expense.user,model_hr_expense_expense,base.group_hr_user,1,1,1,1
access_hr_expense_expense_employee,hr.expense.expense.employee,model_hr_expense_expense,base.group_user,1,1,1,1
access_hr_expense_line_user,hr.expense.line.user,model_hr_expense_line,base.group_hr_user,1,1,1,1
access_hr_expense_line_employee,hr.expense.line.employee,model_hr_expense_line,base.group_user,1,1,1,1
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_report_manager,hr.expense.report.manager,model_hr_expense_report,base.group_hr_manager,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
@@ -11,7 +10,7 @@ access_account_journal_user,account.journal.user,account.model_account_journal,b
access_account_journal_employee,account.journal.employee,account.model_account_journal,base.group_user,1,0,0,0
access_account_invoice_user,account.invoice.user,account.model_account_invoice,base.group_hr_user,1,1,1,1
access_account_invoice_line_user,account.invoice.line.user,account.model_account_invoice_line,base.group_hr_user,1,1,1,1
access_account_invoice_tax_user,account.invoice.tax.user,account.model_account_invoice_tax,base.group_hr_user,1,1,1,1
access_account_move_user,account.move.user,account.model_account_move,base.group_hr_user,1,1,1,1
access_account_move_line_user,account.move.line.user,account.model_account_move_line,base.group_hr_user,1,1,1,1
access_account_analytic_line_user,account.ianalytic.line.user,account.model_account_analytic_line,base.group_hr_user,1,1,1,1
access_account_invoice_tax_user,account.invoice.tax.user,account.model_account_invoice_tax,base.group_hr_user,1,0,0,0
access_account_move_user,account.move.user,account.model_account_move,base.group_hr_user,1,0,0,0
access_account_move_line_user,account.move.line.user,account.model_account_move_line,base.group_hr_user,1,0,0,0
access_account_analytic_line_user,account.analytic.line.user,account.model_account_analytic_line,base.group_hr_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_expense_expense_user access_hr_expense_user hr.expense.expense.user hr.expense.user model_hr_expense_expense model_hr_expense base.group_hr_user 1 1 1 1
3 access_hr_expense_expense_employee access_hr_expense_employee hr.expense.expense.employee hr.expense.employee model_hr_expense_expense model_hr_expense base.group_user 1 1 1 1
4 access_hr_expense_line_user access_hr_expense_report_manager hr.expense.line.user hr.expense.report.manager model_hr_expense_line model_hr_expense_report base.group_hr_user base.group_hr_manager 1 1 1 1
access_hr_expense_line_employee hr.expense.line.employee model_hr_expense_line base.group_user 1 1 1 1
5 access_hr_expense_report_manager access_product_product_hr_expense_user hr.expense.report.manager product.product.hr.expense.user model_hr_expense_report product.model_product_product base.group_hr_manager base.group_hr_user 1 1 1 1
6 access_product_product_hr_expense_user access_product_template_hr_expense_user product.product.hr.expense.user product.template.hr.expense.user product.model_product_product product.model_product_template base.group_hr_user 1 1 1 1
7 access_product_template_hr_expense_user access_product_uom_hr_expense_user product.template.hr.expense.user product.uom.hr.expense.user product.model_product_template product.model_product_uom base.group_hr_user 1 1 1 1
10 access_account_journal_employee access_account_invoice_user account.journal.employee account.invoice.user account.model_account_journal account.model_account_invoice base.group_user base.group_hr_user 1 0 1 0 1 0 1
11 access_account_invoice_user access_account_invoice_line_user account.invoice.user account.invoice.line.user account.model_account_invoice account.model_account_invoice_line base.group_hr_user 1 1 1 1
12 access_account_invoice_line_user access_account_invoice_tax_user account.invoice.line.user account.invoice.tax.user account.model_account_invoice_line account.model_account_invoice_tax base.group_hr_user 1 1 0 1 0 1 0
13 access_account_invoice_tax_user access_account_move_user account.invoice.tax.user account.move.user account.model_account_invoice_tax account.model_account_move base.group_hr_user 1 1 0 1 0 1 0
14 access_account_move_user access_account_move_line_user account.move.user account.move.line.user account.model_account_move account.model_account_move_line base.group_hr_user 1 1 0 1 0 1 0
15 access_account_move_line_user access_account_analytic_line_user account.move.line.user account.analytic.line.user account.model_account_move_line account.model_account_analytic_line base.group_hr_user 1 1 1 1
16
+11 -11
View File
@@ -1,25 +1,25 @@
<?xml version="1.0" ?>
<openerp>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="property_rule_expense_manager" model="ir.rule">
<field name="name">Manager Expense</field>
<field name="model_id" ref="model_hr_expense_expense"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4,ref('base.group_hr_manager')), (4,ref('base.group_hr_user'))]"/>
<field name="model_id" ref="model_hr_expense"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_user')), (4, ref('base.group_hr_manager')), (4, ref('base.group_hr_user'))]"/>
</record>
<record id="property_rule_expense_employee" model="ir.rule">
<field name="name">Employee Expense</field>
<field name="model_id" ref="model_hr_expense_expense"/>
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
<field name="model_id" ref="model_hr_expense"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="hr_expense_comp_rule" model="ir.rule">
<field name="name">Expense multi company rule</field>
<field name="model_id" ref="model_hr_expense_expense"/>
<field name="model_id" ref="model_hr_expense"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
<field name="domain_force">['|',('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
</data>
</openerp>
</odoo>
-8
View File
@@ -1,8 +0,0 @@
-
!record {model: hr.expense.expense, id: sep_expenses}:
employee_id: hr.employee_mit
name : 'John Smith'
-
!record {model: hr.expense.line, id: expense_line}:
product_id: hr_expense.car_travel
name: 'Car Travel Expenses'
@@ -1,34 +0,0 @@
-
In order to test the process of "Employee Expenses" by processing the expenses, I confirm Expense Bill of an employee for September month.
-
!workflow {model: hr.expense.expense, action: confirm, ref: sep_expenses}
-
I check that state is 'Confirm'.
-
!assert {model: hr.expense.expense, id: sep_expenses, severity: error, string: Expense should be in Confirm state}:
- state == 'confirm'
-
I approve the September expenses.
-
!workflow {model: hr.expense.expense, action: validate, ref: sep_expenses}
-
I check that state is 'Approved'
-
!assert {model: hr.expense.expense, id: sep_expenses, severity: error, string: Expense should be in Approved state}:
- state == 'accepted'
-
I make Receipt for the expense.
-
!workflow {model: hr.expense.expense, action: done, ref: sep_expenses}
-
I check receipt details.
-
!python {model: hr.expense.expense}: |
sep_expenses = self.browse(cr, uid, ref("sep_expenses"), context=context)
assert sep_expenses.state == 'done', "Expense should be in 'Done' state."
-
I duplicate the expenses and cancel duplicated.
-
!python {model: hr.expense.expense}: |
duplicate_id = self.copy(cr, uid, ref('sep_expenses'), context=context)
self.expense_canceled(cr, uid, [duplicate_id])
-1
View File
@@ -1 +0,0 @@
from . import test_journal_entries
@@ -1,68 +0,0 @@
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.expense')
self.exp_line_obj = self.registry('hr.expense.line')
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})
# 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 Minh Tran',
'employee_id': self.employee.id,
'employee_payable_account_id': account_payable_id,
})
self.exp_line_obj.create(cr, uid, {
'name': 'Car Travel Expenses',
'product_id': self.product_id,
'unit_amount': 700.00,
'expense_id': self.expense_id
})
def test_journal_entry(self):
cr, uid = self.cr, self.uid
#Submit to Manager
workflow.trg_validate(uid, 'hr.expense.expense', self.expense_id, 'confirm', cr)
#Approve
workflow.trg_validate(uid, 'hr.expense.expense', self.expense_id, 'validate', cr)
#Create Expense Entries
workflow.trg_validate(uid, 'hr.expense.expense', self.expense_id, 'done', cr)
self.expense = self.expense_obj.browse(cr, uid, self.expense_id)
self.assertEquals(self.expense.state, 'done', 'Expense is not in Waiting Payment state')
self.assertTrue(self.expense.account_move_id.id, 'Expense Journal Entry is not created')
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)
@@ -46,16 +46,4 @@
</data>
</field>
</record>
<data noupdate="1">
<!-- After installation of the module, open the related menu -->
<record id="action_client_hr_expense_menu" model="ir.actions.client">
<field name="name">Open HR Menu</field>
<field name="tag">reload</field>
<field name="params" eval="{'menu_id': ref('menu_hr_expense_root')}"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_client_hr_expense_menu"/>
<field name="state">open</field>
</record>
</data>
</odoo>
</odoo>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- After installation of the module, open the related menu -->
<record id="action_client_expense_menu" model="ir.actions.client">
<field name="name">Open Expense Menu</field>
<field name="tag">reload</field>
<field name="params" eval="{'menu_id': ref('menu_hr_expense_root')}"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_client_expense_menu"/>
<field name="state">open</field>
</record>
</data>
</odoo>
@@ -0,0 +1,348 @@
<?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">
<field name="date"/>
<field name="name"/>
<field name="employee_id"/>
<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="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_form_view" model="ir.ui.view">
<field name="name">hr.expense.form</field>
<field name="model">hr.expense</field>
<field eval="25" name="priority"/>
<field name="arch" type="xml">
<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="Generate Accounting 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"}'/>
</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>
<group>
<group>
<field name="product_id" context="{'default_can_be_expensed': 1}"/>
<field name="name"/>
</group>
<group>
<field name="unit_amount" required="1"/>
<label for="quantity"/>
<div>
<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"/>
</group>
</group>
<group class="oe_subtotal_footer oe_right">
<label for="total_amount" class="oe_subtotal_footer_separator" style="padding-top: 6px !important;"/>
<div>
<field name="total_amount" nolabel="1" class="oe_subtotal_footer_separator" widget='monetary' options="{'currency_field': 'currency_id'}"/>
</div>
</group>
<group string="Extra Info">
<group>
<field name="date"/>
<field name="employee_id" invisible="context.get('hide_employee',False)"/>
<field name="department_id" invisible="context.get('hide_employee',False)"/>
<field name="analytic_account_id" string="Contract" groups="analytic.group_analytic_accounting"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="currency_id" groups="base.group_multi_currency"/>
</group>
<group groups="account.group_account_user">
<field name="journal_id" domain="[('type', '=', 'purchase'])]"/>
<label for="payment_mode" string="Payment Done"/>
<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'])]" class="oe_inline"/>
</div>
<field name="account_move_id" domain="[('type', '=', 'purchase'])]" readonly="1"/>
</group>
</group>
<div>
<field name="description" placeholder="Free Notes"/>
</div>
</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="hr_expense_kanban_view" model="ir.ui.view">
<field name="name">hr.expense.kanban</field>
<field name="model">hr.expense</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="name"/>
<field name="employee_id"/>
<field name="total_amount"/>
<field name="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 t-attf-class="pull-right text-right label #{['draft', 'submit'].indexOf(record.state.raw_value) > -1 ? 'label-default' : ['cancel'].indexOf(record.state.raw_value) > -1 ? 'label-danger' : ['post'].indexOf(record.state.raw_value) > -1 ? 'label-warning' : ['done'].indexOf(record.state.raw_value) > -1 ? 'label-success' : 'label-primary'}"><t t-esc="record.state.value"/></span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_hr_expense_filter" model="ir.ui.view">
<field name="name">hr.expense.filter</field>
<field name="model">hr.expense</field>
<field name="arch" type="xml">
<search string="Expense">
<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"/>
<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"/>
<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"/>
<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="Product" domain="[]" context="{'group_by': 'product_id'}"/>
<filter string="Contract" domain="[]" context="{'group_by': 'analytic_account_id'}" groups="analytic.group_analytic_accounting"/>
<filter string="Expenses Month" domain="[]" context="{'group_by': '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="expense_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</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
<field name="context">{'hide_employee': 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_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"/>
</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"/>
</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"/>
</record>
<record id="view_product_hr_expense_form" model="ir.ui.view">
<field name="name">product.template.expense.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<div name="options" position="inside">
<div>
<field name="can_be_expensed"/>
<label for="can_be_expensed"/>
</div>
</div>
</field>
</record>
<record id="hr_expense_product" model="ir.actions.act_window">
<field name="name">Expense Products</field>
<field name="res_model">product.product</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{"default_can_be_expensed": 1}</field>
<field name="domain">[('can_be_expensed', '=', True)]</field>
<field name="search_view_id" ref="product.product_search_form_view"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new expense category.
</p>
</field>
</record>
<record id="action_approved_expense" model="ir.actions.act_window">
<field name="name">Employee Expenses</field>
<field name="res_model">hr.expense</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="view_expenses_tree"/>
<field name="domain">[]</field>
<field name="context">{"search_default_approved": 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="action_request_approve_expense" model="ir.actions.act_window">
<field name="name">Expenses to Approve</field>
<field name="res_model">hr.expense</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form</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="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_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)
</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>
<record id="hr_expense_entry_action_server" model="ir.actions.server">
<field name="name">Generate Single Accounting Entry</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)
</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>
<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" groups="base.group_configuration"/>
<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="hr_expense_action_from_department" model="ir.actions.act_window">
<field name="name">Expenses to Approve</field>
<field name="res_model">hr.expense</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="context">{
'search_default_submitted': 1,
'search_default_department_id': [active_id],
'default_department_id': active_id}
</field>
</record>
</odoo>
+35 -20
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<template id="report_expense">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
@@ -23,40 +22,48 @@
</div>
<div class="col-xs-3">
<strong>Validated By:</strong>
<p t-field="o.user_valid"/>
<p t-field="o.employee_id.parent_id.name"/>
</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 class="text-center">Qty</th>
<th groups="product.group_uom">UoM</th>
<th>Taxe(s)</th>
<th class="text-right">Price</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.line_ids" t-as="line">
<td><span t-field="line.date_value"/></td>
<tr>
<td>
<span t-field="line.name"/>
<span t-field="line.description"/><br/>
<span t-field="line.analytic_account.name"/>
</td>
<td style="text-center">
<span t-field="line.ref"/>
<span t-field="o.name"/>
<span t-field="o.analytic_account_id.name"/>
</td>
<td>
<span t-field="line.unit_amount"/>
<span t-field="o.unit_amount"/>
</td>
<td class="text-center">
<span t-field="line.unit_quantity"/>
<span t-field="o.quantity"/>
</td>
<td groups="product.group_uom">
<span t-field="product_uom_id"/>
</td>
<td>
<t t-foreach="o.tax_ids" t-as="tax">
<t t-if="tax.description">
<span t-field="tax.description"/>
</t>
<t t-if="not tax.description">
<span t-field="tax.name"/>
</t>
</t>
</td>
<td class="text-right">
<span t-field="line.total_amount"
<span t-field="o.total_amount"
t-field-options='{"widget": "monetary", "display_currency":"o.currency_id"}'/>
</td>
</tr>
@@ -69,7 +76,7 @@
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span t-field="o.amount"
<span t-field="o.total_amount"
t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
</td>
</tr>
@@ -77,7 +84,7 @@
</div>
</div>
<p t-field="o.note"/>
<p t-field="o.description"/>
<p>Certified honest and conform,<br/>(Date and signature).<br/><br/></p>
<p>This document must be dated and signed for reimbursement.</p>
</div>
@@ -85,5 +92,13 @@
</t>
</t>
</template>
</data>
</openerp>
<report
id="action_report_hr_expense"
string="HR Expense"
model="hr.expense"
report_type="qweb-pdf"
name="hr_expense.report_expense"
file="hr_expense.report_expense"
/>
</odoo>
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import hr_expense_refuse_reason
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import api, fields, models
class HrExpenseRefuseWizard(models.TransientModel):
_name = "hr.expense.refuse.wizard"
_description = "Hr Expense refuse Reason wizard"
description = fields.Char(string='Reason', required=True)
@api.multi
def expense_refuse_reason(self):
self.ensure_one()
context = dict(self._context or {})
active_ids = context.get('active_ids', [])
expense = self.env['hr.expense'].browse(active_ids)
expense.refuse_expenses(self.description)
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_expense_refuse_wizard_view_form" model="ir.ui.view">
<field name="name">hr.expense.refuse.wizard.form</field>
<field name="model">hr.expense.refuse.wizard</field>
<field name="arch" type="xml">
<form string="Expense refuse reason">
<separator string="Reason to refuse expense."/>
<field name="description"/>
<footer>
<button string='Refuse' name="expense_refuse_reason" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="hr_expense_refuse_wizard_action" model="ir.actions.act_window">
<field name="name">Refuse Expense</field>
<field name="res_model">hr.expense.refuse.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="hr_expense_refuse_wizard_view_form"/>
<field name="target">new</field>
</record>
</odoo>
+5
View File
@@ -139,6 +139,11 @@
</record>
<record id="base.action_attachment" model="ir.actions.act_window">
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to attach new document.
</p>
</field>
</record>
</data>
+23 -31
View File
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.addons.sale.tests.test_sale_common import TestSale
from openerp import workflow
class TestSaleExpense(TestSale):
@@ -25,25 +24,22 @@ class TestSaleExpense(TestSale):
company = self.env.ref('base.main_company')
journal = self.env['account.journal'].create({'name': 'Purchase Journal - Test', 'code': 'HRTPJ', 'type': 'purchase', 'company_id': company.id})
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})
exp = self.env['hr.expense.expense'].create({
'name': 'Test Expense',
'user_id': self.user.id,
'line_ids': [(0, 0, {'product_id': prod_exp.id,
'name': 'Air Travel',
'analytic_account': so.project_id.id,
'unit_amount': 700.0,
})],
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
exp = self.env['hr.expense'].create({
'name': 'Air Travel',
'product_id': prod_exp.id,
'analytic_account_id': so.project_id.id,
'unit_amount': 700.0,
'journal_id': journal.id,
'employee_payable_account_id': account_payable.id,
'employee_id': employee.id,
})
cr, uid = self.cr, self.uid
# Submit to Manager
workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'confirm', cr)
exp.submit_expenses()
# Approve
workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'validate', cr)
exp.approve_expenses()
# Create Expense Entries
workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'done', cr)
exp.action_move_create()
# expense should now be in sales order
self.assertTrue(prod_exp 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.id)
@@ -53,26 +49,22 @@ class TestSaleExpense(TestSale):
# create some expense and validate it (expense at sales price)
init_price = so.amount_total
prod_exp = self.env.ref('hr_expense.car_travel')
exp = self.env['hr.expense.expense'].create({
'name': 'Test Expense',
'user_id': self.user.id,
'line_ids': [(0, 0, {'product_id': prod_exp.id,
'name': 'Car Travel',
'analytic_account': so.project_id.id,
'uom_id': self.env.ref('product.product_uom_km').id,
'unit_amount': 0.15,
'unit_quantity': 100,
})],
exp = self.env['hr.expense'].create({
'name': 'Car Travel',
'product_id': prod_exp.id,
'analytic_account_id': so.project_id.id,
'product_uom_id': self.env.ref('product.product_uom_km').id,
'unit_amount': 0.15,
'quantity': 100,
'journal_id': journal.id,
'employee_payable_account_id': account_payable.id,
'employee_id': employee.id,
})
# Submit to Manager
workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'confirm', cr)
exp.submit_expenses()
# Approve
workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'validate', cr)
exp.approve_expenses()
# Create Expense Entries
workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'done', cr)
exp.action_move_create()
# expense should now be in sales order
self.assertTrue(prod_exp 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.id)
@@ -83,4 +75,4 @@ class TestSaleExpense(TestSale):
# both expenses should be invoiced
inv_id = so.action_invoice_create()
inv = self.env['account.invoice'].browse(inv_id)
self.assertEqual(inv.amount_total, 732.0, 'Sale Expense: invoicing of exepense is wrong')
self.assertEqual(inv.amount_total, 732.0, 'Sale Expense: invoicing of expense is wrong')