96a37c0e448182e3b32b8abdeb095a7f2377d4c7
[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),
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 By'),
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 or '/',
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 be Expensed', help="Specify if the product can be selected in an HR expense line."),
237     }
238
239 product_product()
240
241 class hr_expense_line(osv.osv):
242     _name = "hr.expense.line"
243     _description = "Expense Line"
244
245     def _amount(self, cr, uid, ids, field_name, arg, context=None):
246         if not ids:
247             return {}
248         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),))
249         res = dict(cr.fetchall())
250         return res
251
252     def _get_uom_id(self, cr, uid, context=None):
253         result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'product', 'product_uom_unit')
254         return result and result[1] or False
255
256     _columns = {
257         'name': fields.char('Expense Note', size=128, required=True),
258         'date_value': fields.date('Date', required=True),
259         'expense_id': fields.many2one('hr.expense.expense', 'Expense', ondelete='cascade', select=True),
260         'total_amount': fields.function(_amount, string='Total', digits_compute=dp.get_precision('Account')),
261         'unit_amount': fields.float('Unit Price', digits_compute=dp.get_precision('Product Price')),
262         'unit_quantity': fields.float('Quantities', digits_compute= dp.get_precision('Product Unit of Measure')),
263         'product_id': fields.many2one('product.product', 'Product', domain=[('hr_expense_ok','=',True)]),
264         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True),
265         'description': fields.text('Description'),
266         'analytic_account': fields.many2one('account.analytic.account','Analytic account'),
267         'ref': fields.char('Reference', size=32),
268         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of expense lines."),
269         }
270     _defaults = {
271         'unit_quantity': 1,
272         'date_value': lambda *a: time.strftime('%Y-%m-%d'),
273         'uom_id': _get_uom_id,
274     }
275     _order = "sequence, date_value desc"
276
277     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
278         res = {}
279         if product_id:
280             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
281             res['name'] = product.name
282             amount_unit = product.price_get('standard_price')[product.id]
283             res['unit_amount'] = amount_unit
284             res['uom_id'] = product.uom_id.id
285         return {'value': res}
286
287     def onchange_uom(self, cr, uid, ids, product_id, uom_id, context=None):
288         res = {'value':{}}
289         if not uom_id or not product_id:
290             return res
291         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
292         uom = self.pool.get('product.uom').browse(cr, uid, uom_id, context=context)
293         if uom.category_id.id != product.uom_id.category_id.id:
294             res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')}
295             res['value'].update({'uom_id': product.uom_id.id})
296         return res
297
298 hr_expense_line()
299
300 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: