[IMP]hr_expense:code improved
[odoo/odoo.git] / addons / hr_expense / hr_expense.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23
24 from osv import fields, osv
25 from tools.translate import _
26 import decimal_precision as dp
27 import netsvc
28
29 def _employee_get(obj, cr, uid, context=None):
30     if context is None:
31         context = {}
32     ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
33     if ids:
34         return ids[0]
35     return False
36
37 class hr_expense_expense(osv.osv):
38
39     def copy(self, cr, uid, id, default=None, context=None):
40         if context is None:
41             context = {}
42         if not default: default = {}
43         default.update({'voucher_id': False, 'date_confirm': False, 'date_valid': False, 'user_valid': False})
44         return super(hr_expense_expense, self).copy(cr, uid, id, default, context=context)
45
46     def _amount(self, cr, uid, ids, field_name, arg, context=None):
47         res= {}
48         for expense in self.browse(cr, uid, ids, context=context):
49             total = 0.0
50             for line in expense.line_ids:
51                 total += line.unit_amount * line.unit_quantity
52             res[expense.id] = total
53         return res
54
55     def _get_currency(self, cr, uid, context=None):
56         user = self.pool.get('res.users').browse(cr, uid, [uid], context=context)[0]
57         if user.company_id:
58             return user.company_id.currency_id.id
59         else:
60             return self.pool.get('res.currency').search(cr, uid, [('rate','=',1.0)], context=context)[0]
61
62     _name = "hr.expense.expense"
63     _inherit = ['mail.thread']
64     _description = "Expense"
65     _order = "id desc"
66     _columns = {
67         'name': fields.char('Description', size=128, required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
68         'id': fields.integer('Sheet ID', readonly=True),
69         'date': fields.date('Date', select=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
70         'journal_id': fields.many2one('account.journal', 'Force Journal', help = "The journal used when the expense is done."),
71         'employee_id': fields.many2one('hr.employee', "Employee", required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
72         'user_id': fields.many2one('res.users', 'User', required=True),
73         'date_confirm': fields.date('Confirmation Date', select=True, help="Date of the confirmation of the sheet expense. It's filled when the button Confirm is pressed."),
74         'date_valid': fields.date('Validation Date', select=True, help="Date of the acceptation of the sheet expense. It's filled when the button Accept is pressed."),
75         'user_valid': fields.many2one('res.users', 'Validation By', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
76         'account_move_id': fields.many2one('account.move', 'Ledger Posting'),
77         'line_ids': fields.one2many('hr.expense.line', 'expense_id', 'Expense Lines', readonly=True, states={'draft':[('readonly',False)]} ),
78         'note': fields.text('Note'),
79         'amount': fields.function(_amount, string='Total Amount', digits_compute= dp.get_precision('Account')),
80         'voucher_id': fields.many2one('account.voucher', "Employee's Receipt"),
81         'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
82         'department_id':fields.many2one('hr.department','Department', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
83         'company_id': fields.many2one('res.company', 'Company', required=True),
84         'state': fields.selection([
85             ('draft', 'New'),
86             ('cancelled', 'Refused'),
87             ('confirm', 'Waiting Approval'),
88             ('accepted', 'Approved'),
89             ('done', 'Done'),
90             ],
91             'Status', readonly=True, 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\'.\
92             \nIf the admin accepts it, the status is \'Accepted\'.\n If a receipt is made for the expense request, the status is \'Done\'.'),
93     }
94     _defaults = {
95         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.employee', context=c),
96         'date': fields.date.context_today,
97         'state': 'draft',
98         'employee_id': _employee_get,
99         'user_id': lambda cr, uid, id, c={}: id,
100         'currency_id': _get_currency,
101     }
102
103     def unlink(self, cr, uid, ids, context=None):
104         for rec in self.browse(cr, uid, ids, context=context):
105             if rec.state != 'draft':
106                 raise osv.except_osv(_('Warning!'),_('You can only delete draft expenses!'))
107         return super(hr_expense_expense, self).unlink(cr, uid, ids, context)
108
109     def onchange_currency_id(self, cr, uid, ids, currency_id=False, company_id=False, context=None):
110         res =  {'value': {'journal_id': False}}
111         journal_ids = self.pool.get('account.journal').search(cr, uid, [('type','=','purchase'), ('currency','=',currency_id), ('company_id', '=', company_id)], context=context)
112         if journal_ids:
113             res['value']['journal_id'] = journal_ids[0]
114         return res
115
116     def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
117         emp_obj = self.pool.get('hr.employee')
118         department_id = False
119         company_id = False
120         if employee_id:
121             employee = emp_obj.browse(cr, uid, employee_id, context=context)
122             department_id = employee.department_id.id
123             company_id = employee.company_id.id
124         return {'value': {'department_id': department_id, 'company_id': company_id}}
125
126     def expense_confirm(self, cr, uid, ids, *args):
127         for expense in self.browse(cr, uid, ids):
128             if expense.employee_id and expense.employee_id.parent_id.user_id:
129                 self.message_subscribe_users(cr, uid, [expense.id], user_ids=[expense.employee_id.parent_id.user_id.id])
130         self.expense_toapprove_notificate(cr, uid, ids)
131         self.write(cr, uid, ids, {
132             'state':'confirm',
133             'date_confirm': time.strftime('%Y-%m-%d')
134         })
135         return True
136
137     def expense_accept(self, cr, uid, ids, *args):
138         self.expense_approve_notificate(cr, uid, ids)
139         self.write(cr, uid, ids, {
140             'state':'accepted',
141             'date_valid':time.strftime('%Y-%m-%d'),
142             'user_valid': uid,
143             })
144         return True
145
146     def expense_canceled(self, cr, uid, ids, *args):
147         self.expense_refuse_notificate(cr, uid, ids)
148         self.write(cr, uid, ids, {'state':'cancelled'})
149         return True
150
151     def action_receipt_create(self, cr, uid, ids, context=None):
152         property_obj = self.pool.get('ir.property')
153         sequence_obj = self.pool.get('ir.sequence')
154         analytic_journal_obj = self.pool.get('account.analytic.journal')
155         account_journal = self.pool.get('account.journal')
156         voucher_obj = self.pool.get('account.voucher')
157         currency_obj = self.pool.get('res.currency')
158         wkf_service = netsvc.LocalService("workflow")
159         if context is None:
160             context = {}
161         for exp in self.browse(cr, uid, ids, context=context):
162             company_id = exp.company_id.id
163             lines = []
164             total = 0.0
165             ctx = context.copy()
166             ctx.update({'date': exp.date})
167             journal = False
168             if exp.journal_id:
169                 journal = exp.journal_id
170             else:
171                 journal_id = voucher_obj._get_journal(cr, uid, context={'type': 'purchase', 'company_id': company_id})
172                 if journal_id:
173                     journal = account_journal.browse(cr, uid, journal_id, context=context)
174             for line in exp.line_ids:
175                 if line.product_id:
176                     acc = line.product_id.product_tmpl_id.property_account_expense
177                     if not acc:
178                         acc = line.product_id.categ_id.property_account_expense_categ
179                 else:
180                     acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company_id})
181                     if not acc:
182                         raise osv.except_osv(_('Error!'), _('Please configure Default Expense account for Product purchase: `property_account_expense_categ`.'))
183                 total_amount = line.total_amount
184                 if journal.currency:
185                     if exp.currency_id != journal.currency:
186                         total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, journal.currency.id, total_amount, context=ctx)
187                 elif exp.currency_id != exp.company_id.currency_id:
188                     total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, exp.company_id.currency_id.id, total_amount, context=ctx)
189                 lines.append((0, False, {
190                     'name': line.name,
191                     'account_id': acc.id,
192                     'account_analytic_id': line.analytic_account.id,
193                     'amount': total_amount,
194                     'type': 'dr'
195                 }))
196                 total += total_amount
197             if not exp.employee_id.address_home_id:
198                 raise osv.except_osv(_('Error!'), _('The employee must have a home address.'))
199             acc = exp.employee_id.address_home_id.property_account_payable.id
200             voucher = {
201                 'name': exp.name or '/',
202                 'reference': sequence_obj.get(cr, uid, 'hr.expense.invoice'),
203                 'account_id': acc,
204                 'type': 'purchase',
205                 'partner_id': exp.employee_id.address_home_id.id,
206                 'company_id': company_id,
207                 'line_ids': lines,
208                 'amount': total,
209                 'journal_id': journal.id,
210             }
211             if journal and not journal.analytic_journal_id:
212                 analytic_journal_ids = analytic_journal_obj.search(cr, uid, [('type','=','purchase')], context=context)
213                 if analytic_journal_ids:
214                     account_journal.write(cr, uid, [journal.id], {'analytic_journal_id': analytic_journal_ids[0]}, context=context)
215             voucher_id = voucher_obj.create(cr, uid, voucher, context=context)
216             wkf_service.trg_validate(uid, 'account.voucher', voucher_id, 'proforma_voucher', cr)
217             self.write(cr, uid, [exp.id], {'voucher_id': voucher_id, 'state': 'done'}, context=context)
218         return True
219     
220     def action_view_receipt(self, cr, uid, ids, context=None):
221         '''
222         This function returns an action that display existing receipt of given expense ids.
223         '''
224         assert len(ids) == 1, 'This option should only be used for a single id at a time'
225         voucher_id = self.browse(cr, uid, ids[0], context=context).voucher_id.id
226         res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_voucher', 'view_purchase_receipt_form')
227         result = {
228             'name': _('Expense Receipt'),
229             'view_type': 'form',
230             'view_mode': 'form',
231             'view_id': res and res[1] or False,
232             'res_model': 'account.voucher',
233             'type': 'ir.actions.act_window',
234             'nodestroy': True,
235             'target': 'current',
236             'res_id': voucher_id,
237         }
238         return result
239
240     def expense_toapprove_notificate(self, cr, uid, ids, context=None):
241         for obj in self.browse(cr, uid, ids):
242             self.message_post(cr, uid, [obj.id],
243                 _("The request is <b>waiting for Approval</b>"), subtype="hr_expense.mt_expense_approve", context=context)
244
245     def expense_approve_notificate(self, cr, uid, ids, context=None):
246         for obj in self.browse(cr, uid, ids):
247             self.message_post(cr, uid, [obj.id],
248                 _("The request has been <b>approved</b>"), subtype="hr_expense.mt_expense_approved", context=context)
249
250     def expense_refuse_notificate(self, cr, uid, ids, context=None):
251         for obj in self.browse(cr, uid, ids):
252             self.message_post(cr, uid, [obj.id],
253                 _("Request <b>refused</b>"), subtype="hr_expense.mt_expense_refused", context=context)
254
255 hr_expense_expense()
256
257 class product_product(osv.osv):
258     _inherit = "product.product"
259     _columns = {
260         'hr_expense_ok': fields.boolean('Can be Expensed', help="Specify if the product can be selected in an HR expense line."),
261     }
262
263 product_product()
264
265 class hr_expense_line(osv.osv):
266     _name = "hr.expense.line"
267     _description = "Expense Line"
268
269     def _amount(self, cr, uid, ids, field_name, arg, context=None):
270         if not ids:
271             return {}
272         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),))
273         res = dict(cr.fetchall())
274         return res
275
276     def _get_uom_id(self, cr, uid, context=None):
277         result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'product', 'product_uom_unit')
278         return result and result[1] or False
279
280     _columns = {
281         'name': fields.char('Expense Note', size=128, required=True),
282         'date_value': fields.date('Date', required=True),
283         'expense_id': fields.many2one('hr.expense.expense', 'Expense', ondelete='cascade', select=True),
284         'total_amount': fields.function(_amount, string='Total', digits_compute=dp.get_precision('Account')),
285         'unit_amount': fields.float('Unit Price', digits_compute=dp.get_precision('Product Price')),
286         'unit_quantity': fields.float('Quantities', digits_compute= dp.get_precision('Product Unit of Measure')),
287         'product_id': fields.many2one('product.product', 'Product', domain=[('hr_expense_ok','=',True)]),
288         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True),
289         'description': fields.text('Description'),
290         'analytic_account': fields.many2one('account.analytic.account','Analytic account'),
291         'ref': fields.char('Reference', size=32),
292         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of expense lines."),
293         }
294     _defaults = {
295         'unit_quantity': 1,
296         'date_value': lambda *a: time.strftime('%Y-%m-%d'),
297         'uom_id': _get_uom_id,
298     }
299     _order = "sequence, date_value desc"
300
301     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
302         res = {}
303         if product_id:
304             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
305             res['name'] = product.name
306             amount_unit = product.price_get('standard_price')[product.id]
307             res['unit_amount'] = amount_unit
308             res['uom_id'] = product.uom_id.id
309         return {'value': res}
310
311     def onchange_uom(self, cr, uid, ids, product_id, uom_id, context=None):
312         res = {'value':{}}
313         if not uom_id or not product_id:
314             return res
315         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
316         uom = self.pool.get('product.uom').browse(cr, uid, uom_id, context=context)
317         if uom.category_id.id != product.uom_id.category_id.id:
318             res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')}
319             res['value'].update({'uom_id': product.uom_id.id})
320         return res
321
322 hr_expense_line()
323
324 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: