[IMP]Improve a tooltip.
[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),
68         'id': fields.integer('Sheet ID', readonly=True),
69         'date': fields.date('Date', select=True),
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),
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 User'),
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),
82         'department_id':fields.many2one('hr.department','Department'),
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 onchange_currency_id(self, cr, uid, ids, currency_id=False, company_id=False, context=None):
104         res =  {'value': {'journal_id': False}}
105         journal_ids = self.pool.get('account.journal').search(cr, uid, [('type','=','purchase'), ('currency','=',currency_id), ('company_id', '=', company_id)], context=context)
106         if journal_ids:
107             res['value']['journal_id'] = journal_ids[0]
108         return res
109
110     def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
111         emp_obj = self.pool.get('hr.employee')
112         department_id = False
113         company_id = False
114         if employee_id:
115             employee = emp_obj.browse(cr, uid, employee_id, context=context)
116             department_id = employee.department_id.id
117             company_id = employee.company_id.id
118         return {'value': {'department_id': department_id, 'company_id': company_id}}
119
120     def expense_confirm(self, cr, uid, ids, *args):
121         for expense in self.browse(cr, uid, ids):
122             if expense.employee_id and expense.employee_id.parent_id.user_id:
123                 self.message_subscribe_users(cr, uid, [expense.id], user_ids=[expense.employee_id.parent_id.user_id.id])
124         self.write(cr, uid, ids, {
125             'state':'confirm',
126             'date_confirm': time.strftime('%Y-%m-%d')
127         })
128         return True
129
130     def expense_accept(self, cr, uid, ids, *args):
131         self.write(cr, uid, ids, {
132             'state':'accepted',
133             'date_valid':time.strftime('%Y-%m-%d'),
134             'user_valid': uid,
135             })
136         return True
137
138     def expense_canceled(self, cr, uid, ids, *args):
139         self.write(cr, uid, ids, {'state':'cancelled'})
140         return True
141
142     def action_receipt_create(self, cr, uid, ids, context=None):
143         property_obj = self.pool.get('ir.property')
144         sequence_obj = self.pool.get('ir.sequence')
145         analytic_journal_obj = self.pool.get('account.analytic.journal')
146         account_journal = self.pool.get('account.journal')
147         voucher_obj = self.pool.get('account.voucher')
148         currency_obj = self.pool.get('res.currency')
149         wkf_service = netsvc.LocalService("workflow")
150         if context is None:
151             context = {}
152         for exp in self.browse(cr, uid, ids, context=context):
153             company_id = exp.company_id.id
154             lines = []
155             total = 0.0
156             ctx = context.copy()
157             ctx.update({'date': exp.date})
158             journal = False
159             if exp.journal_id:
160                 journal = exp.journal_id
161             else:
162                 journal_id = voucher_obj._get_journal(cr, uid, context={'type': 'purchase', 'company_id': company_id})
163                 if journal_id:
164                     journal = account_journal.browse(cr, uid, journal_id, context=context)
165             for line in exp.line_ids:
166                 if line.product_id:
167                     acc = line.product_id.product_tmpl_id.property_account_expense
168                     if not acc:
169                         acc = line.product_id.categ_id.property_account_expense_categ
170                 else:
171                     acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company_id})
172                     if not acc:
173                         raise osv.except_osv(_('Error!'), _('Please configure Default Expense account for Product purchase: `property_account_expense_categ`.'))
174                 total_amount = line.total_amount
175                 if journal.currency:
176                     if exp.currency_id != journal.currency:
177                         total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, journal.currency.id, total_amount, context=ctx)
178                 elif exp.currency_id != exp.company_id.currency_id:
179                     total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, exp.company_id.currency_id.id, total_amount, context=ctx)
180                 lines.append((0, False, {
181                     'name': line.name,
182                     'account_id': acc.id,
183                     'account_analytic_id': line.analytic_account.id,
184                     'amount': total_amount,
185                     'type': 'dr'
186                 }))
187                 total += total_amount
188             if not exp.employee_id.address_home_id:
189                 raise osv.except_osv(_('Error!'), _('The employee must have a home address.'))
190             acc = exp.employee_id.address_home_id.property_account_payable.id
191             voucher = {
192                 'name': exp.name,
193                 'reference': sequence_obj.get(cr, uid, 'hr.expense.invoice'),
194                 'account_id': acc,
195                 'type': 'purchase',
196                 'partner_id': exp.employee_id.address_home_id.id,
197                 'company_id': company_id,
198                 'line_ids': lines,
199                 'amount': total,
200                 'journal_id': journal.id,
201             }
202             if journal and not journal.analytic_journal_id:
203                 analytic_journal_ids = analytic_journal_obj.search(cr, uid, [('type','=','purchase')], context=context)
204                 if analytic_journal_ids:
205                     account_journal.write(cr, uid, [journal.id], {'analytic_journal_id': analytic_journal_ids[0]}, context=context)
206             voucher_id = voucher_obj.create(cr, uid, voucher, context=context)
207             wkf_service.trg_validate(uid, 'account.voucher', voucher_id, 'proforma_voucher', cr)
208             self.write(cr, uid, [exp.id], {'voucher_id': voucher_id, 'state': 'done'}, context=context)
209         return True
210     
211     def action_view_receipt(self, cr, uid, ids, context=None):
212         '''
213         This function returns an action that display existing receipt of given expense ids.
214         '''
215         assert len(ids) == 1, 'This option should only be used for a single id at a time'
216         voucher_id = self.browse(cr, uid, ids[0], context=context).voucher_id.id
217         res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_voucher', 'view_purchase_receipt_form')
218         result = {
219             'name': _('Expense Receipt'),
220             'view_type': 'form',
221             'view_mode': 'form',
222             'view_id': res and res[1] or False,
223             'res_model': 'account.voucher',
224             'type': 'ir.actions.act_window',
225             'nodestroy': True,
226             'target': 'current',
227             'res_id': voucher_id,
228         }
229         return result
230
231 hr_expense_expense()
232
233 class product_product(osv.osv):
234     _inherit = "product.product"
235     _columns = {
236         'hr_expense_ok': fields.boolean('Can Constitute an Expense', help="Specify if the product can be selected in a HR expense line."),
237     }
238
239     def on_change_hr_expense_ok(self, cr, uid, id, hr_expense_ok):
240
241         if not hr_expense_ok:
242             return {}
243         data_obj = self.pool.get('ir.model.data')
244         cat_id = data_obj._get_id(cr, uid, 'hr_expense', 'cat_expense')
245         categ_id = data_obj.browse(cr, uid, cat_id).res_id
246         res = {'value' : {'type':'service','sale_ok' :False,'categ_id':categ_id }}
247         return res
248
249 product_product()
250
251 class hr_expense_line(osv.osv):
252     _name = "hr.expense.line"
253     _description = "Expense Line"
254
255     def _amount(self, cr, uid, ids, field_name, arg, context=None):
256         if not ids:
257             return {}
258         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),))
259         res = dict(cr.fetchall())
260         return res
261
262     def _get_uom_id(self, cr, uid, context=None):
263         result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'product', 'product_uom_unit')
264         return result and result[1] or False
265
266     _columns = {
267         'name': fields.char('Expense Note', size=128, required=True),
268         'date_value': fields.date('Date', required=True),
269         'expense_id': fields.many2one('hr.expense.expense', 'Expense', ondelete='cascade', select=True),
270         'total_amount': fields.function(_amount, string='Total', digits_compute=dp.get_precision('Account')),
271         'unit_amount': fields.float('Unit Price', digits_compute=dp.get_precision('Product Price')),
272         'unit_quantity': fields.float('Quantities', digits_compute= dp.get_precision('Product Unit of Measure')),
273         'product_id': fields.many2one('product.product', 'Product', domain=[('hr_expense_ok','=',True)]),
274         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True),
275         'description': fields.text('Description'),
276         'analytic_account': fields.many2one('account.analytic.account','Analytic account'),
277         'ref': fields.char('Reference', size=32),
278         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of expense lines."),
279         }
280     _defaults = {
281         'unit_quantity': 1,
282         'date_value': lambda *a: time.strftime('%Y-%m-%d'),
283         'uom_id': _get_uom_id,
284     }
285     _order = "sequence, date_value desc"
286
287     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
288         res = {}
289         if product_id:
290             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
291             res['name'] = product.name
292             amount_unit = product.price_get('standard_price')[product.id]
293             res['unit_amount'] = amount_unit
294             res['uom_id'] = product.uom_id.id
295         return {'value': res}
296
297     def onchange_uom(self, cr, uid, ids, product_id, uom_id, context=None):
298         res = {'value':{}}
299         if not uom_id or not product_id:
300             return res
301         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
302         uom = self.pool.get('product.uom').browse(cr, uid, uom_id, context=context)
303         if uom.category_id.id != product.uom_id.category_id.id:
304             res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')}
305             res['value'].update({'uom_id': product.uom_id.id})
306         return res
307
308 hr_expense_line()
309
310 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: