[MERGE] merge from trunk addons
[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 import netsvc
25 from osv import fields, osv
26 from tools.translate import _
27
28 def _employee_get(obj, cr, uid, context=None):
29     if context is None:
30         context = {}
31     ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
32     if ids:
33         return ids[0]
34     return False
35
36 class hr_expense_expense(osv.osv):
37
38     def copy(self, cr, uid, id, default=None, context=None):
39         if context is None:
40             context = {}
41         if not default: default = {}
42         default.update({'invoice_id': False, 'date_confirm': False, 'date_valid': False, 'user_valid': False})
43         return super(hr_expense_expense, self).copy(cr, uid, id, default, context=context)
44
45     def _amount(self, cr, uid, ids, field_name, arg, context=None):
46         cr.execute("SELECT s.id,COALESCE(SUM(l.unit_amount*l.unit_quantity),0) AS amount FROM hr_expense_expense s LEFT OUTER JOIN hr_expense_line l ON (s.id=l.expense_id) WHERE s.id IN %s GROUP BY s.id ", (tuple(ids),))
47         res = dict(cr.fetchall())
48         return res
49
50     def _get_currency(self, cr, uid, context=None):
51         user = self.pool.get('res.users').browse(cr, uid, [uid], context=context)[0]
52         if user.company_id:
53             return user.company_id.currency_id.id
54         else:
55             return self.pool.get('res.currency').search(cr, uid, [('rate','=',1.0)], context=context)[0]
56
57     _name = "hr.expense.expense"
58     _description = "Expense"
59     _columns = {
60         'name': fields.char('Description', size=128, required=True),
61         'id': fields.integer('Sheet ID', readonly=True),
62         'ref': fields.char('Reference', size=32),
63         'date': fields.date('Date'),
64         'journal_id': fields.many2one('account.journal', 'Force Journal', help = "The journal used when the expense is invoiced"),
65         'employee_id': fields.many2one('hr.employee', "Employee", required=True),
66         'user_id': fields.many2one('res.users', 'User', required=True),
67         'date_confirm': fields.date('Confirmation Date', help = "Date of the confirmation of the sheet expense. It's filled when the button Confirm is pressed."),
68         'date_valid': fields.date('Validation Date', help = "Date of the acceptation of the sheet expense. It's filled when the button Accept is pressed."),
69         'user_valid': fields.many2one('res.users', 'Validation User'),
70         'account_move_id': fields.many2one('account.move', 'Ledger Posting'),
71         'line_ids': fields.one2many('hr.expense.line', 'expense_id', 'Expense Lines', readonly=True, states={'draft':[('readonly',False)]} ),
72         'note': fields.text('Note'),
73         'amount': fields.function(_amount, method=True, string='Total Amount'),
74         'invoice_id': fields.many2one('account.invoice', "Employee's Invoice"),
75         'currency_id': fields.many2one('res.currency', 'Currency', required=True),
76         'department_id':fields.many2one('hr.department','Department'),
77         'company_id': fields.many2one('res.company', 'Company', required=True),
78         'state': fields.selection([
79             ('draft', 'Draft'),
80             ('confirm', 'Waiting Approval'),
81             ('accepted', 'Approved'),
82             ('invoiced', 'Invoiced'),
83             ('paid', 'Reimbursed'),
84             ('cancelled', 'Refused')],
85             'State', readonly=True, help='When the expense request is created the state is \'Draft\'.\n It is confirmed by the user and request is sent to admin, the state is \'Waiting Confirmation\'.\
86             \nIf the admin accepts it, the state is \'Accepted\'.\n If an invoice is made for the expense request, the state is \'Invoiced\'.\n If the expense is paid to user, the state is \'Reimbursed\'.'),
87     }
88     _defaults = {
89         'date': lambda *a: time.strftime('%Y-%m-%d'),
90         'state': 'draft',
91         'employee_id': _employee_get,
92         'user_id': lambda cr, uid, id, c={}: id,
93         'currency_id': _get_currency,
94         'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
95     }
96
97     def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
98         department_id = False
99         if employee_id:
100             department_id = self.pool.get('hr.employee').browse(cr, uid, employee_id, context=context).department_id.id or False
101         return {'value':{'department_id':department_id}}
102
103     def expense_confirm(self, cr, uid, ids, *args):
104         self.write(cr, uid, ids, {
105             'state':'confirm',
106             'date_confirm': time.strftime('%Y-%m-%d')
107         })
108         return True
109
110     def expense_accept(self, cr, uid, ids, *args):
111         self.write(cr, uid, ids, {
112             'state':'accepted',
113             'date_valid':time.strftime('%Y-%m-%d'),
114             'user_valid': uid,
115             })
116         return True
117
118     def expense_canceled(self, cr, uid, ids, *args):
119         self.write(cr, uid, ids, {'state':'cancelled'})
120         return True
121
122     def expense_paid(self, cr, uid, ids, *args):
123         self.write(cr, uid, ids, {'state':'paid'})
124         return True
125
126     def action_invoice_create(self, cr, uid, ids):
127         res = False
128         invoice_obj = self.pool.get('account.invoice')
129         property_obj = self.pool.get('ir.property')
130         sequence_obj = self.pool.get('ir.sequence')
131         analytic_journal_obj = self.pool.get('account.analytic.journal')
132         account_journal = self.pool.get('account.journal')
133         for exp in self.browse(cr, uid, ids):
134             lines = []
135             for l in exp.line_ids:
136                 tax_id = []
137                 if l.product_id:
138                     acc = l.product_id.product_tmpl_id.property_account_expense
139                     if not acc:
140                         acc = l.product_id.categ_id.property_account_expense_categ
141                     tax_id = [x.id for x in l.product_id.supplier_taxes_id]
142                 else:
143                     acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category')
144                     if not acc:
145                         raise osv.except_osv(_('Error !'), _('Please configure Default Expense account for Product purchase, `property_account_expense_categ`'))
146
147                 lines.append((0, False, {
148                     'name': l.name,
149                     'account_id': acc.id,
150                     'price_unit': l.unit_amount,
151                     'quantity': l.unit_quantity,
152                     'uos_id': l.uom_id.id,
153                     'product_id': l.product_id and l.product_id.id or False,
154                     'invoice_line_tax_id': tax_id and [(6, 0, tax_id)] or False,
155                     'account_analytic_id': l.analytic_account.id,
156                 }))
157             if not exp.employee_id.address_home_id:
158                 raise osv.except_osv(_('Error !'), _('The employee must have a Home address'))
159             acc = exp.employee_id.address_home_id.partner_id.property_account_payable.id
160             payment_term_id = exp.employee_id.address_home_id.partner_id.property_payment_term.id
161             inv = {
162                 'name': exp.name,
163                 'reference': sequence_obj.get(cr, uid, 'hr.expense.invoice'),
164                 'account_id': acc,
165                 'type': 'in_invoice',
166                 'partner_id': exp.employee_id.address_home_id.partner_id.id,
167                 'address_invoice_id': exp.employee_id.address_home_id.id,
168                 'address_contact_id': exp.employee_id.address_home_id.id,
169                 'origin': exp.name,
170                 'invoice_line': lines,
171                 'currency_id': exp.currency_id.id,
172                 'payment_term': payment_term_id,
173                 'fiscal_position': exp.employee_id.address_home_id.partner_id.property_account_position.id
174             }
175             if payment_term_id:
176                 to_update = invoice_obj.onchange_payment_term_date_invoice(cr, uid, [], payment_term_id, None)
177                 if to_update:
178                     inv.update(to_update['value'])
179             journal = False
180             if exp.journal_id:
181                 inv['journal_id']=exp.journal_id.id
182                 journal = exp.journal_id
183             else:
184                 journal_id = invoice_obj._get_journal(cr, uid, context={'type': 'in_invoice'})
185                 if journal_id:
186                     inv['journal_id'] = journal_id
187                     journal = account_journal.browse(cr, uid, journal_id)
188             if journal and not journal.analytic_journal_id:
189                 analytic_journal_ids = analytic_journal_obj.search(cr, uid, [('type','=','purchase')])
190                 if analytic_journal_ids:
191                     account_journal.write(cr, uid, [journal.id],{'analytic_journal_id':analytic_journal_ids[0]})
192             inv_id = invoice_obj.create(cr, uid, inv, {'type': 'in_invoice'})
193             invoice_obj.button_compute(cr, uid, [inv_id], {'type': 'in_invoice'}, set_total=True)
194             wf_service = netsvc.LocalService("workflow")
195             wf_service.trg_validate(uid, 'account.invoice', inv_id, 'invoice_open', cr)
196
197             self.write(cr, uid, [exp.id], {'invoice_id': inv_id, 'state': 'invoiced'})
198             res = inv_id
199         return res
200
201 hr_expense_expense()
202
203 class product_product(osv.osv):
204     _inherit = "product.product"
205     _columns = {
206         'hr_expense_ok': fields.boolean('Can Constitute an Expense', help="Determines if the product can be visible in the list of product within a selection from an HR expense sheet line."),
207     }
208
209 product_product()
210
211 class hr_expense_line(osv.osv):
212     _name = "hr.expense.line"
213     _description = "Expense Line"
214
215     def _amount(self, cr, uid, ids, field_name, arg, context=None):
216         if not ids:
217             return {}
218         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),))
219         res = dict(cr.fetchall())
220         return res
221
222     _columns = {
223         'name': fields.char('Expense Note', size=128, required=True),
224         'date_value': fields.date('Date', required=True),
225         'expense_id': fields.many2one('hr.expense.expense', 'Expense', ondelete='cascade', select=True),
226         'total_amount': fields.function(_amount, method=True, string='Total'),
227         'unit_amount': fields.float('Unit Price'),
228         'unit_quantity': fields.float('Quantities' ),
229         'product_id': fields.many2one('product.product', 'Product', domain=[('hr_expense_ok','=',True)]),
230         'uom_id': fields.many2one('product.uom', 'UoM' ),
231         'description': fields.text('Description'),
232         'analytic_account': fields.many2one('account.analytic.account','Analytic account'),
233         'ref': fields.char('Reference', size=32),
234         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of expense lines."),
235         }
236     _defaults = {
237         'unit_quantity': 1,
238         'date_value': lambda *a: time.strftime('%Y-%m-%d'),
239     }
240     _order = "sequence, date_value desc"
241
242     def onchange_product_id(self, cr, uid, ids, product_id, uom_id, employee_id, context=None):
243         if context is None:
244             ctx = {}
245         else:
246             # we only want to update it locally
247             ctx = context.copy()
248
249         res = {}
250         if product_id:
251             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
252             res['name'] = product.name
253             # Compute based on pricetype of employee company
254             ctx['currency_id'] = self.pool.get('hr.employee').browse(cr, uid, employee_id, context=context).user_id.company_id.currency_id.id
255             amount_unit = product.price_get('standard_price', ctx)[product.id]
256             res['unit_amount'] = amount_unit
257             if not uom_id:
258                 res['uom_id'] = product.uom_id.id
259         return {'value': res}
260
261 hr_expense_line()
262
263 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: