1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
24 from openerp import netsvc
25 from openerp.osv import fields, osv
26 from openerp.tools.translate import _
28 import openerp.addons.decimal_precision as dp
30 def _employee_get(obj, cr, uid, context=None):
33 ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
38 class hr_expense_expense(osv.osv):
40 def copy(self, cr, uid, id, default=None, context=None):
43 if not default: default = {}
44 default.update({'voucher_id': False, 'date_confirm': False, 'date_valid': False, 'user_valid': False})
45 return super(hr_expense_expense, self).copy(cr, uid, id, default, context=context)
47 def _amount(self, cr, uid, ids, field_name, arg, context=None):
49 for expense in self.browse(cr, uid, ids, context=context):
51 for line in expense.line_ids:
52 total += line.unit_amount * line.unit_quantity
53 res[expense.id] = total
56 def _get_currency(self, cr, uid, context=None):
57 user = self.pool.get('res.users').browse(cr, uid, [uid], context=context)[0]
59 return user.company_id.currency_id.id
61 return self.pool.get('res.currency').search(cr, uid, [('rate','=',1.0)], context=context)[0]
63 _name = "hr.expense.expense"
64 _inherit = ['mail.thread']
65 _description = "Expense"
69 'hr_expense.mt_expense_approved': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'accepted',
70 'hr_expense.mt_expense_refused': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'cancelled',
71 'hr_expense.mt_expense_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'confirm',
76 'name': fields.char('Description', size=128, required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
77 'id': fields.integer('Sheet ID', readonly=True),
78 'date': fields.date('Date', select=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
79 'journal_id': fields.many2one('account.journal', 'Force Journal', help = "The journal used when the expense is done."),
80 'employee_id': fields.many2one('hr.employee', "Employee", required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
81 'user_id': fields.many2one('res.users', 'User', required=True),
82 '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."),
83 '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."),
84 'user_valid': fields.many2one('res.users', 'Validation By', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
85 'account_move_id': fields.many2one('account.move', 'Ledger Posting'),
86 'line_ids': fields.one2many('hr.expense.line', 'expense_id', 'Expense Lines', readonly=True, states={'draft':[('readonly',False)]} ),
87 'note': fields.text('Note'),
88 'amount': fields.function(_amount, string='Total Amount', digits_compute=dp.get_precision('Account')),
89 'voucher_id': fields.many2one('account.voucher', "Employee's Receipt"),
90 'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
91 'department_id':fields.many2one('hr.department','Department', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
92 'company_id': fields.many2one('res.company', 'Company', required=True),
93 'state': fields.selection([
95 ('cancelled', 'Refused'),
96 ('confirm', 'Waiting Approval'),
97 ('accepted', 'Approved'),
100 'Status', readonly=True, track_visibility='onchange',
101 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\'.\
102 \nIf the admin accepts it, the status is \'Accepted\'.\n If a receipt is made for the expense request, the status is \'Done\'.'),
105 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.employee', context=c),
106 'date': fields.date.context_today,
108 'employee_id': _employee_get,
109 'user_id': lambda cr, uid, id, c={}: id,
110 'currency_id': _get_currency,
113 def unlink(self, cr, uid, ids, context=None):
114 for rec in self.browse(cr, uid, ids, context=context):
115 if rec.state != 'draft':
116 raise osv.except_osv(_('Warning!'),_('You can only delete draft expenses!'))
117 return super(hr_expense_expense, self).unlink(cr, uid, ids, context)
119 def onchange_currency_id(self, cr, uid, ids, currency_id=False, company_id=False, context=None):
120 res = {'value': {'journal_id': False}}
121 journal_ids = self.pool.get('account.journal').search(cr, uid, [('type','=','purchase'), ('currency','=',currency_id), ('company_id', '=', company_id)], context=context)
123 res['value']['journal_id'] = journal_ids[0]
126 def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
127 emp_obj = self.pool.get('hr.employee')
128 department_id = False
131 employee = emp_obj.browse(cr, uid, employee_id, context=context)
132 department_id = employee.department_id.id
133 company_id = employee.company_id.id
134 return {'value': {'department_id': department_id, 'company_id': company_id}}
136 def expense_confirm(self, cr, uid, ids, context=None):
137 for expense in self.browse(cr, uid, ids):
138 if expense.employee_id and expense.employee_id.parent_id.user_id:
139 self.message_subscribe_users(cr, uid, [expense.id], user_ids=[expense.employee_id.parent_id.user_id.id])
140 return self.write(cr, uid, ids, {'state': 'confirm', 'date_confirm': time.strftime('%Y-%m-%d')}, context=context)
142 def expense_accept(self, cr, uid, ids, context=None):
143 return self.write(cr, uid, ids, {'state': 'accepted', 'date_valid': time.strftime('%Y-%m-%d'), 'user_valid': uid}, context=context)
145 def expense_canceled(self, cr, uid, ids, context=None):
146 return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
149 def account_move_get(self, cr, uid, expense_id, journal_id, context=None):
151 This method prepare the creation of the account move related to the given expense.
153 :param expense_id: Id of voucher for which we are creating account_move.
154 :return: mapping between fieldname and value of account move to create
159 #Search for the period corresponding with confirmation date
160 expense_brw = self.browse(cr,uid,expense_id,context)
161 period_obj = self.pool.get('account.period')
162 company_id = expense_brw.company_id.id
164 ctx.update({'company_id': company_id})
165 date = expense_brw.date_confirm
166 pids = period_obj.find(cr, uid, date, context=ctx)
170 raise osv.except_osv(_('Error! '),
171 _('Please define periods!'))
172 period = period_obj.browse(cr, uid, period_id, context=context)
174 seq_obj = self.pool.get('ir.sequence')
176 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
177 if journal.sequence_id:
178 if not journal.sequence_id.active:
179 raise osv.except_osv(_('Configuration Error !'),
180 _('Please activate the sequence of selected journal !'))
182 c.update({'fiscalyear_id': period.fiscalyear_id.id})
183 name = seq_obj.next_by_id(cr, uid, journal.sequence_id.id, context=c)
185 raise osv.except_osv(_('Error!'),
186 _('Please define a sequence on the journal.'))
187 #Look for the next expense number
188 ref = seq_obj.get(cr, uid, 'hr.expense.invoice')
192 'journal_id': journal_id,
194 'date': expense_brw.date_confirm,
196 'period_id': period_id,
200 def line_get_convert(self, cr, uid, x, part, date, context=None):
202 'date_maturity': x.get('date_maturity', False),
203 'partner_id': part.id,
204 'name': x['name'][:64],
206 'debit': x['price']>0 and x['price'],
207 'credit': x['price']<0 and -x['price'],
208 'account_id': x['account_id'],
209 'analytic_lines': x.get('analytic_lines', False),
210 'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
211 'currency_id': x.get('currency_id', False),
212 'tax_code_id': x.get('tax_code_id', False),
213 'tax_amount': x.get('tax_amount', False),
214 'ref': x.get('ref', False),
215 'quantity': x.get('quantity',1.00),
216 'product_id': x.get('product_id', False),
217 'product_uom_id': x.get('uos_id', False),
218 'analytic_account_id': x.get('account_analytic_id', False),
221 def compute_expense_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines, context=None):
226 cur_obj = self.pool.get('res.currency')
227 for i in invoice_move_lines:
228 if inv.currency_id.id != company_currency:
229 context.update({'date': inv.date_confirm or time.strftime('%Y-%m-%d')})
230 i['currency_id'] = inv.currency_id.id
231 i['amount_currency'] = i['price']
232 i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
233 company_currency, i['price'],
236 i['amount_currency'] = False
237 i['currency_id'] = False
240 total_currency -= i['amount_currency'] or i['price']
241 return total, total_currency, invoice_move_lines
244 def action_move_create(self, cr, uid, ids, context=None):
245 property_obj = self.pool.get('ir.property')
246 sequence_obj = self.pool.get('ir.sequence')
247 analytic_journal_obj = self.pool.get('account.analytic.journal')
248 account_journal = self.pool.get('account.journal')
249 voucher_obj = self.pool.get('account.voucher')
250 currency_obj = self.pool.get('res.currency')
251 ait_obj = self.pool.get('account.invoice.tax')
252 move_obj = self.pool.get('account.move')
255 for exp in self.browse(cr, uid, ids, context=context):
256 company_id = exp.company_id.id
260 ctx.update({'date': exp.date})
263 journal = exp.journal_id
265 journal_id = voucher_obj._get_journal(cr, uid, context={'type': 'purchase', 'company_id': company_id})
267 journal = account_journal.browse(cr, uid, journal_id, context=context)
269 raise osv.except_osv(_('Error!'), _("No expense journal found. Please make sure you have a journal with type 'purchase' configured."))
270 if not journal.sequence_id:
271 raise osv.except_osv(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
272 company_currency = exp.company_id.currency_id.id
273 current_currency = exp.currency_id
274 # for line in exp.line_ids:
275 # if line.product_id:
276 # acc = line.product_id.property_account_expense
278 # acc = line.product_id.categ_id.property_account_expense_categ
280 # acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company_id})
282 # raise osv.except_osv(_('Error!'), _('Please configure Default Expense account for Product purchase: `property_account_expense_categ`.'))
283 # total_amount = line.total_amount
284 # if journal.currency:
285 # if exp.currency_id != journal.currency:
286 # total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, journal.currency.id, total_amount, context=ctx)
287 # elif exp.currency_id != exp.company_id.currency_id:
288 # total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, exp.company_id.currency_id.id, total_amount, context=ctx)
289 # lines.append((0, False, {
291 # 'account_id': acc.id,
292 # 'account_analytic_id': line.analytic_account.id,
293 # 'amount': total_amount,
296 # total += total_amount
297 if not exp.employee_id.address_home_id:
298 raise osv.except_osv(_('Error!'), _('The employee must have a home address.'))
299 acc = exp.employee_id.address_home_id.property_account_payable.id
301 #From action_move_line_create of voucher
302 move_id = move_obj.create(cr, uid, self.account_move_get(cr, uid, exp.id, journal.id, context=context), context=context)
303 move = move_obj.browse(cr, uid, move_id, context=context)
304 #iml = self._get_analytic_lines
306 # within: iml = self.pool.get('account.invoice.line').move_line_get
307 iml = self.move_line_get(cr, uid, exp.id, context=context)
310 diff_currency_p = exp.currency_id.id <> company_currency
311 # create one move line for the total
314 total, total_currency, iml = self.compute_expense_totals(cr, uid, exp, company_currency, exp.name, iml, context=ctx)
317 #Need to have counterline:
324 'date_maturity': exp.date_confirm,
325 'amount_currency': diff_currency_p and total_currency or False,
326 'currency_id': diff_currency_p and exp.currency_id.id or False,
330 line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, exp.user_id.partner_id, exp.date_confirm, context=ctx)),iml)
331 move_obj.write(cr, uid, [move_id], {'line_id':line}, context=ctx)
332 self.write(cr, uid, ids, {'account_move_id':move_id, 'state':'done'}, context=context)
335 #compute_taxes = ait_obj.compute(cr, uid, , context=context)
341 # 'name':line.name.split('\n')[0][:64],
342 # 'price_unit':line.price_unit,
343 # 'quantity':line.quantity,
344 # 'price':line.price_subtotal,
345 # 'account_id':line.account_id.id,
346 # 'product_id':line.product_id.id,
347 # 'uos_id':line.uos_id.id,
348 # 'account_analytic_id':line.account_analytic_id.id,
349 # 'taxes':line.invoice_line_tax_id}
352 def move_line_get(self, cr, uid, expense_id, context=None):
354 tax_obj = self.pool.get('account.tax')
355 cur_obj = self.pool.get('res.currency')
358 exp = self.browse(cr, uid, expense_id, context=context)
359 company_currency = exp.company_id.currency_id.id
361 for line in exp.line_ids:
362 mres = self.move_line_get_item(cr, uid, line, context)
366 tax_code_found= False
368 #Calculate tax according to default tax on product
370 #Taken from product_id_onchange in account.invoice
373 fpos_obj = self.pool.get('account.fiscal.position')
374 fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
375 product = line.product_id
376 taxes = product.supplier_taxes_id
377 #If taxes are not related to the product, maybe they are in the account
379 a = product.property_account_expense.id #Why is not there a check here?
381 a = product.categ_id.property_account_expense_categ.id
382 a = fpos_obj.map_account(cr, uid, fpos, a)
383 taxes = a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False
384 tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
387 #Calculating tax on the line and creating move?
388 for tax in tax_obj.compute_all(cr, uid, taxes,
390 line.unit_quantity, line.product_id,
391 exp.user_id.partner_id)['taxes']:
392 tax_code_id = tax['base_code_id']
393 tax_amount = line.total_amount * tax['base_sign']
397 res.append(self.move_line_get_item(cr, uid, line, context))
398 res[-1]['price'] = 0.0
399 res[-1]['account_analytic_id'] = False
400 elif not tax_code_id:
402 tax_code_found = True
403 res[-1]['tax_code_id'] = tax_code_id
404 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, exp.currency_id.id, company_currency, tax_amount, context={'date': exp.date_confirm})
406 #Will create the tax here as we don't have the access
410 'price_unit': tax['price_unit'],
412 'price': tax['amount'] * tax['base_sign'] or 0.0,
413 'account_id': tax['account_collected_id'],
414 'tax_code_id': tax['tax_code_id'],
415 'tax_amount': tax['amount'] * tax['base_sign'],
417 res.append(assoc_tax)
420 def move_line_get_item(self, cr, uid, line, context=None):
421 company = line.expense_id.company_id
422 property_obj = self.pool.get('ir.property')
424 acc = line.product_id.property_account_expense
426 acc = line.product_id.categ_id.property_account_expense_categ
428 acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company.id})
430 raise osv.except_osv(_('Error!'), _('Please configure Default Expense account for Product purchase: `property_account_expense_categ`.'))
433 'name': line.name.split('\n')[0][:64],
434 'price_unit':line.unit_amount,
435 'quantity':line.unit_quantity,
436 'price':line.total_amount,
438 'product_id':line.product_id.id,
439 'uos_id':line.uom_id.id,
440 'account_analytic_id':line.analytic_account.id,
441 #'taxes':line.invoice_line_tax_id,
448 def action_receipt_create(self, cr, uid, ids, context=None):
449 property_obj = self.pool.get('ir.property')
450 sequence_obj = self.pool.get('ir.sequence')
451 analytic_journal_obj = self.pool.get('account.analytic.journal')
452 account_journal = self.pool.get('account.journal')
453 voucher_obj = self.pool.get('account.voucher')
454 currency_obj = self.pool.get('res.currency')
455 wkf_service = netsvc.LocalService("workflow")
458 for exp in self.browse(cr, uid, ids, context=context):
459 company_id = exp.company_id.id
463 ctx.update({'date': exp.date})
466 journal = exp.journal_id
468 journal_id = voucher_obj._get_journal(cr, uid, context={'type': 'purchase', 'company_id': company_id})
470 journal = account_journal.browse(cr, uid, journal_id, context=context)
472 raise osv.except_osv(_('Error!'), _("No expense journal found. Please make sure you have a journal with type 'purchase' configured."))
473 for line in exp.line_ids:
475 acc = line.product_id.property_account_expense
477 acc = line.product_id.categ_id.property_account_expense_categ
479 acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company_id})
481 raise osv.except_osv(_('Error!'), _('Please configure Default Expense account for Product purchase: `property_account_expense_categ`.'))
482 total_amount = line.total_amount
484 if exp.currency_id != journal.currency:
485 total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, journal.currency.id, total_amount, context=ctx)
486 elif exp.currency_id != exp.company_id.currency_id:
487 total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, exp.company_id.currency_id.id, total_amount, context=ctx)
488 lines.append((0, False, {
490 'account_id': acc.id,
491 'account_analytic_id': line.analytic_account.id,
492 'amount': total_amount,
495 total += total_amount
496 if not exp.employee_id.address_home_id:
497 raise osv.except_osv(_('Error!'), _('The employee must have a home address.'))
498 acc = exp.employee_id.address_home_id.property_account_payable.id
500 'name': exp.name or '/',
501 'reference': sequence_obj.get(cr, uid, 'hr.expense.invoice'),
504 'partner_id': exp.employee_id.address_home_id.id,
505 'company_id': company_id,
508 'journal_id': journal.id,
510 if journal and not journal.analytic_journal_id:
511 analytic_journal_ids = analytic_journal_obj.search(cr, uid, [('type','=','purchase')], context=context)
512 if analytic_journal_ids:
513 account_journal.write(cr, uid, [journal.id], {'analytic_journal_id': analytic_journal_ids[0]}, context=context)
514 voucher_id = voucher_obj.create(cr, uid, voucher, context=context)
515 self.write(cr, uid, [exp.id], {'voucher_id': voucher_id, 'state': 'done'}, context=context)
518 def action_view_receipt(self, cr, uid, ids, context=None):
520 This function returns an action that display existing receipt of given expense ids.
522 assert len(ids) == 1, 'This option should only be used for a single id at a time'
523 voucher_id = self.browse(cr, uid, ids[0], context=context).voucher_id.id
524 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_voucher', 'view_purchase_receipt_form')
526 'name': _('Expense Receipt'),
529 'view_id': res and res[1] or False,
530 'res_model': 'account.voucher',
531 'type': 'ir.actions.act_window',
534 'res_id': voucher_id,
540 class product_product(osv.osv):
541 _inherit = "product.product"
543 'hr_expense_ok': fields.boolean('Can be Expensed', help="Specify if the product can be selected in an HR expense line."),
548 class hr_expense_line(osv.osv):
549 _name = "hr.expense.line"
550 _description = "Expense Line"
552 def _amount(self, cr, uid, ids, field_name, arg, context=None):
555 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),))
556 res = dict(cr.fetchall())
559 def _get_uom_id(self, cr, uid, context=None):
560 result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'product', 'product_uom_unit')
561 return result and result[1] or False
564 'name': fields.char('Expense Note', size=128, required=True),
565 'date_value': fields.date('Date', required=True),
566 'expense_id': fields.many2one('hr.expense.expense', 'Expense', ondelete='cascade', select=True),
567 'total_amount': fields.function(_amount, string='Total', digits_compute=dp.get_precision('Account')),
568 'unit_amount': fields.float('Unit Price', digits_compute=dp.get_precision('Product Price')),
569 'unit_quantity': fields.float('Quantities', digits_compute= dp.get_precision('Product Unit of Measure')),
570 'product_id': fields.many2one('product.product', 'Product', domain=[('hr_expense_ok','=',True)]),
571 'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True),
572 'description': fields.text('Description'),
573 'analytic_account': fields.many2one('account.analytic.account','Analytic account'),
574 'ref': fields.char('Reference', size=32),
575 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of expense lines."),
579 'date_value': lambda *a: time.strftime('%Y-%m-%d'),
580 'uom_id': _get_uom_id,
582 _order = "sequence, date_value desc"
584 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
587 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
588 res['name'] = product.name
589 amount_unit = product.price_get('standard_price')[product.id]
590 res['unit_amount'] = amount_unit
591 res['uom_id'] = product.uom_id.id
592 return {'value': res}
594 def onchange_uom(self, cr, uid, ids, product_id, uom_id, context=None):
596 if not uom_id or not product_id:
598 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
599 uom = self.pool.get('product.uom').browse(cr, uid, uom_id, context=context)
600 if uom.category_id.id != product.uom_id.category_id.id:
601 res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure')}
602 res['value'].update({'uom_id': product.uom_id.id})
607 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: