[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:
committed by
qdp-odoo
parent
4ce20c449f
commit
52d72e61de
@@ -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
|
||||
|
||||
@@ -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>
|
||||
+2
-2
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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')
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hr_expense_report
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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 +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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user