[REFACTOR]some change in run_scheduler and working renew contract button
[odoo/odoo.git] / addons / fleet / fleet.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 from osv import osv, fields
23 import time
24 import datetime
25 import tools
26 from osv.orm import except_orm
27 from tools.translate import _
28 from dateutil.relativedelta import relativedelta
29
30 def str_to_date(strdate):
31     return datetime.datetime.strptime(strdate, tools.DEFAULT_SERVER_DATE_FORMAT)
32
33 class fleet_vehicle_cost(osv.Model):
34     _name = 'fleet.vehicle.cost'
35     _description = 'Cost related to a vehicle'
36     _order = 'date desc, vehicle_id asc'
37
38     def _get_odometer(self, cr, uid, ids, odometer_id, arg, context):
39         res = dict.fromkeys(ids, False)
40         for record in self.browse(cr,uid,ids,context=context):
41             if record.odometer_id:
42                 res[record.id] = record.odometer_id.value
43         return res
44
45     def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
46         if not value:
47             raise except_orm(_('Operation not allowed!'), _('Emptying the odometer value of a vehicle is not allowed.'))
48         date = self.browse(cr, uid, id, context=context).date
49         if not(date):
50             date = fields.date.context_today(self, cr, uid, context=context)
51         vehicle_id = self.browse(cr, uid, id, context=context).vehicle_id
52         data = {'value': value, 'date': date, 'vehicle_id': vehicle_id.id}
53         odometer_id = self.pool.get('fleet.vehicle.odometer').create(cr, uid, data, context=context)
54         return self.write(cr, uid, id, {'odometer_id': odometer_id}, context=context)
55
56     def _year_get_fnc(self, cr, uid, ids, name, unknow_none, context=None):
57         res = {}
58         for record in self.browse(cr, uid, ids, context=context):
59             res[record.id] = str(time.strptime(record.date, tools.DEFAULT_SERVER_DATE_FORMAT).tm_year) #TODO: why is it a char?
60         return res
61
62     def _cost_name_get_fnc(self, cr, uid, ids, name, unknow_none, context=None):
63         res = {}
64         for record in self.browse(cr, uid, ids, context=context):
65             name = record.vehicle_id.name
66             if record.cost_subtype.name:
67                 name += ' / '+ record.cost_subtype.name
68             if record.date:
69                 name += ' / '+ record.date
70             res[record.id] = name
71         return res
72
73     _columns = {
74         'name': fields.function(_cost_name_get_fnc, type="char", string='Name', store=True),
75         'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True, help='Vehicle concerned by this log'),
76         'cost_subtype': fields.many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost'),
77         'amount': fields.float('Total Price'),
78         'cost_type': fields.selection([('contract', 'Contract'), ('services','Services'), ('fuel','Fuel'), ('other','Other')], 'Category of the cost', help='For internal purpose only', required=True),
79         'parent_id': fields.many2one('fleet.vehicle.cost', 'Parent', help='Parent cost to this current cost'),
80         'cost_ids': fields.one2many('fleet.vehicle.cost', 'parent_id', 'Included Services'),
81         'odometer_id': fields.many2one('fleet.vehicle.odometer', 'Odometer', help='Odometer measure of the vehicle at the moment of this log'),
82         'odometer': fields.function(_get_odometer, fnct_inv=_set_odometer, type='float', string='Odometer Value', help='Odometer measure of the vehicle at the moment of this log'),
83         'odometer_unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
84         'date' :fields.date('Date',help='Date when the cost has been executed'),
85         'contract_id': fields.many2one('fleet.vehicle.log.contract', 'Contract', help='Contract attached to this cost'),
86         'auto_generated': fields.boolean('automatically generated', readonly=True, required=True),
87         'year': fields.function(_year_get_fnc, type="char", string='Year', store=True),
88     }
89
90     _defaults ={
91         'cost_type': 'other',
92     }
93
94     def create(self, cr, uid, data, context=None):
95         #TODO: should be managed by onchanges() rather by this
96         if 'parent_id' in data and data['parent_id']:
97             parent = self.browse(cr, uid, data['parent_id'], context=context)
98             data['vehicle_id'] = parent.vehicle_id.id
99             data['date'] = parent.date
100             data['cost_type'] = parent.cost_type
101         if 'contract_id' in data and data['contract_id']:
102             contract = self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, data['contract_id'], context=context)
103             data['vehicle_id'] = contract.vehicle_id.id
104             data['cost_subtype'] = contract.cost_subtype.id
105             data['cost_type'] = contract.cost_type
106         return super(fleet_vehicle_cost, self).create(cr, uid, data, context=context)
107
108
109 class fleet_vehicle_tag(osv.Model):
110     _name = 'fleet.vehicle.tag'
111     _columns = {
112         'name': fields.char('Name', required=True, translate=True),
113     }
114
115
116 class fleet_vehicle_state(osv.Model):
117     _name = 'fleet.vehicle.state'
118     _order = 'sequence asc'
119     _columns = {
120         'name': fields.char('Name', required=True),
121         'sequence': fields.integer('Order', help="Used to order the note stages")
122     }
123     _sql_constraints = [('fleet_state_name_unique','unique(name)', 'State name already exists')]
124
125
126 class fleet_vehicle_model(osv.Model):
127
128     def _model_name_get_fnc(self, cr, uid, ids, field_name, arg, context=None):
129         res = {}
130         for record in self.browse(cr, uid, ids, context=context):
131             name = record.modelname
132             if record.brand.name:
133                 name = record.brand.name+' / '+name
134             res[record.id] = name
135         return res
136
137     def on_change_brand(self, cr, uid, ids, model_id, context=None):
138         if not model_id:
139             return {'value': {'image_medium': False}}
140         brand = self.pool.get('fleet.vehicle.model.brand').browse(cr, uid, model_id, context=context)
141         return {
142             'value': {
143                 'image_medium': brand.image,
144             }
145         }
146
147     _name = 'fleet.vehicle.model'
148     _description = 'Model of a vehicle'
149     _order = 'name asc'
150
151     _columns = {
152         'name': fields.function(_model_name_get_fnc, type="char", string='Name', store=True),
153         'modelname': fields.char('Model name', size=32, required=True), 
154         'brand': fields.many2one('fleet.vehicle.model.brand', 'Model Brand', required=True, help='Brand of the vehicle'),
155         'vendors': fields.many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors'),
156         'image': fields.related('brand', 'image', type="binary", string="Logo"),
157         'image_medium': fields.related('brand', 'image_medium', type="binary", string="Logo"),
158         'image_small': fields.related('brand', 'image_small', type="binary", string="Logo"),
159     }
160
161
162 class fleet_vehicle_model_brand(osv.Model):
163     _name = 'fleet.vehicle.model.brand'
164     _description = 'Brand model of the vehicle'
165
166     _order = 'name asc'
167
168     def _get_image(self, cr, uid, ids, name, args, context=None):
169         result = dict.fromkeys(ids, False)
170         for obj in self.browse(cr, uid, ids, context=context):
171             result[obj.id] = tools.image_get_resized_images(obj.image)
172         return result
173
174     def _set_image(self, cr, uid, id, name, value, args, context=None):
175         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
176
177     _columns = {
178         'name': fields.char('Brand Name', size=64, required=True),
179         'image': fields.binary("Logo",
180             help="This field holds the image used as logo for the brand, limited to 1024x1024px."),
181         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
182             string="Medium-sized photo", type="binary", multi="_get_image",
183             store = {
184                 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
185             },
186             help="Medium-sized logo of the brand. It is automatically "\
187                  "resized as a 128x128px image, with aspect ratio preserved. "\
188                  "Use this field in form views or some kanban views."),
189         'image_small': fields.function(_get_image, fnct_inv=_set_image,
190             string="Smal-sized photo", type="binary", multi="_get_image",
191             store = {
192                 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
193             },
194             help="Small-sized photo of the brand. It is automatically "\
195                  "resized as a 64x64px image, with aspect ratio preserved. "\
196                  "Use this field anywhere a small image is required."),
197     }
198
199
200 class fleet_vehicle(osv.Model):
201
202     _inherit = 'mail.thread'
203
204     def _vehicle_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
205         res = {}
206         for record in self.browse(cr, uid, ids, context=context):
207             res[record.id] = record.model_id.brand.name + '/' + record.model_id.modelname + ' / ' + record.license_plate
208         return res
209
210     def return_action_to_open(self, cr, uid, ids, context=None):
211         """ This opens the xml view specified in xml_id for the current vehicle """
212         if context['xml_id']:
213             res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet', context['xml_id'], context=context)
214             res['context'] = {
215                 'default_vehicle_id': ids[0]
216             }
217             res['domain']=[('vehicle_id','=', ids[0])]
218             return res
219         else:
220             return False
221
222     def act_show_log_cost(self, cr, uid, ids, context=None):
223         """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
224             @return: the costs log view
225         """
226         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet','fleet_vehicle_costs_act', context)
227         res['context'] = {
228             'default_vehicle_id': ids[0],
229             'search_default_parent_false' : True
230         }
231         res['domain']=[('vehicle_id','=', ids[0])]
232         return res
233
234     def _get_odometer(self, cr, uid, ids, odometer_id, arg, context):
235         res = dict.fromkeys(ids, 0)
236         for record in self.browse(cr,uid,ids,context=context):
237             ids = self.pool.get('fleet.vehicle.odometer').search(cr, uid, [('vehicle_id', '=', record.id)], limit=1, order='value desc')
238             if len(ids) > 0:
239                 res[record.id] = self.pool.get('fleet.vehicle.odometer').browse(cr, uid, ids[0], context=context).value
240         return res
241
242     def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
243         if value:
244             date = fields.date.context_today(self, cr, uid, context=context)
245             data = {'value': value, 'date': date, 'vehicle_id': id}
246             return self.pool.get('fleet.vehicle.odometer').create(cr, uid, data, context=context)
247
248     def _search_get_overdue_contract_reminder(self, cr, uid, obj, name, args, context):
249         res = []
250         for field, operator, value in args:
251             vehicle_ids = self.search(cr, uid, [])
252             contracts_needed = self._get_contract_reminder_fnc(cr,uid,vehicle_ids,['contract_renewal_total', 'contract_renewal_due_soon', 'contract_renewal_overdue', 'contract_renewal_name'],None,context=context)
253             res_ids = []
254             for renew_key,renew_value in contracts_needed.items():
255                 if eval(str(renew_value['contract_renewal_overdue']) + " " + str(operator) + " " + str(value)):
256                     res_ids.append(renew_key)
257             res.append(('id', 'in', res_ids))      
258         return res
259     
260     def _search_contract_renewal_due_soon(self, cr, uid, obj, name, args, context):
261         res = []
262         for field, operator, value in args:
263             vehicle_ids = self.search(cr, uid, [])
264             contracts_needed = self._get_contract_reminder_fnc(cr,uid,vehicle_ids,['contract_renewal_total', 'contract_renewal_due_soon', 'contract_renewal_overdue', 'contract_renewal_name'],None,context=context)
265             res_ids = []
266             for renew_key,renew_value in contracts_needed.items():
267                 if eval(str(renew_value['contract_renewal_due_soon']) + " " + str(operator) + " " + str(value)):
268                     res_ids.append(renew_key)
269             res.append(('id', 'in', res_ids))      
270         return res
271
272     def _get_contract_reminder_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
273         res= {}
274         for record in self.browse(cr, uid, ids, context=context):
275             overdue = 0
276             due_soon = 0
277             name = ''
278             for element in record.log_contracts:
279                 if (element.state in ('open', 'toclose') and element.expiration_date):
280                     current_date_str = fields.date.context_today(self, cr, uid, context=context)
281                     due_time_str = element.expiration_date
282                     current_date = str_to_date(current_date_str)
283                     due_time = str_to_date(due_time_str)
284                     diff_time = (due_time-current_date).days
285                     if diff_time < 0:
286                         overdue += 1
287                     if diff_time<15 and diff_time>=0:
288                             due_soon = due_soon +1;
289                     if overdue+due_soon>0:
290                         ids = self.pool.get('fleet.vehicle.log.contract').search(cr,uid,[('vehicle_id','=',record.id),'|',('state','=','open'),('state','=','toclose')],limit=1,order='expiration_date asc')
291                         if len(ids) > 0:
292                             name=(self.pool.get('fleet.vehicle.log.contract').browse(cr,uid,ids[0],context=context).cost_subtype.name)
293
294             res[record.id] = {
295                 'contract_renewal_overdue':overdue,
296                 'contract_renewal_due_soon':due_soon,
297                 'contract_renewal_total':(overdue+due_soon-1),
298                 'contract_renewal_name':name,
299             }
300         return res
301
302     def run_scheduler(self, cr, uid, context=None):
303         datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
304         limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
305         ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, ['&', ('state', '=', 'open'), ('expiration_date', '<', limit_date)], offset=0, limit=None, order=None, context=context, count=False)
306         res = {}
307         for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, ids, context=context):
308             if contract.vehicle_id.id in res:
309                 res[contract.vehicle_id.id] += 1
310             else:
311                 res[contract.vehicle_id.id] = 1
312
313         for vehicle, value in res.items():
314             self.message_post(cr, uid, vehicle, body=_('%s contract(s) need(s) to be renewed and/or closed!') % (str(value)), context=context)
315
316         return self.pool.get('fleet.vehicle.log.contract').write(cr, uid, ids, {'state': 'toclose'}, context=context)
317
318     def _get_default_state(self, cr, uid, context):
319         try:
320             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'vehicle_state_active')
321         except ValueError:
322             model_id = False
323         return model_id
324
325     _name = 'fleet.vehicle'
326     _description = 'Information on a vehicle'
327     _order= 'license_plate asc'
328     _columns = {
329         'name': fields.function(_vehicle_name_get_fnc, type="char", string='Name', store=True),
330         'company_id': fields.many2one('res.company', 'Company'),
331         'license_plate': fields.char('License Plate', size=32, required=True, help='License plate number of the vehicle (ie: plate number for a car)'),
332         'vin_sn': fields.char('Chassis Number', size=32, help='Unique number written on the vehicle motor (VIN/SN number)'),
333         'driver': fields.many2one('res.partner', 'Driver', help='Driver of the vehicle'),
334         'model_id': fields.many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle'),
335         'log_fuel': fields.one2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs'),
336         'log_services': fields.one2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs'),
337         'log_contracts': fields.one2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts'),
338         'acquisition_date': fields.date('Acquisition Date', required=False, help='Date when the vehicle has been bought'),
339         'color': fields.char('Color', size=32, help='Color of the vehicle'),
340         'state': fields.many2one('fleet.vehicle.state', 'State', help='Current state of the vehicle', ondelete="set null"),
341         'location': fields.char('Location', size=128, help='Location of the vehicle (garage, ...)'),
342         'seats': fields.integer('Seats Number', help='Number of seats of the vehicle'),
343         'doors': fields.integer('Doors Number', help='Number of doors of the vehicle'),
344         'tag_ids' :fields.many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id','tag_id', 'Tags'),
345         'odometer': fields.function(_get_odometer, fnct_inv=_set_odometer, type='float', string='Odometer Value', help='Odometer measure of the vehicle at the moment of this log'),
346         'odometer_unit': fields.selection([('kilometers', 'Kilometers'),('miles','Miles')], 'Odometer Unit', help='Unit of the odometer ',required=True),
347         'transmission': fields.selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle'),
348         'fuel_type': fields.selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle'),
349         'horsepower': fields.integer('Horsepower'),
350         'horsepower_tax': fields.float('Horsepower Taxation'),
351         'power': fields.integer('Power (kW)', help='Power in kW of the vehicle'),
352         'co2': fields.float('CO2 Emissions', help='CO2 emissions of the vehicle'),
353         'image': fields.related('model_id', 'image', type="binary", string="Logo"),
354         'image_medium': fields.related('model_id', 'image_medium', type="binary", string="Logo"),
355         'image_small': fields.related('model_id', 'image_small', type="binary", string="Logo"),
356         'contract_renewal_due_soon': fields.function(_get_contract_reminder_fnc, fnct_search=_search_contract_renewal_due_soon, type="integer", string='Contracts to renew', multi='contract_info'),
357         'contract_renewal_overdue': fields.function(_get_contract_reminder_fnc, fnct_search=_search_get_overdue_contract_reminder, type="integer", string='Contracts Overdued', multi='contract_info'),
358         'contract_renewal_name': fields.function(_get_contract_reminder_fnc, type="text", string='Name of contract to renew soon', multi='contract_info'),
359         'contract_renewal_total': fields.function(_get_contract_reminder_fnc, type="integer", string='Total of contracts due or overdue minus one', multi='contract_info'),
360         'car_value': fields.float('Car Value', help='Value of the bought vehicle'),
361         }
362
363     _defaults = {
364         'doors': 5,
365         'odometer_unit': 'kilometers',
366         'state': _get_default_state,
367     }
368
369     def copy(self, cr, uid, id, default=None, context=None):
370         if not default:
371             default = {}
372         default.update({
373             'log_fuel':[],
374             'log_contracts':[],
375             'log_services':[],
376             'tag_ids':[],
377             'vin_sn':'',
378         })
379         return super(fleet_vehicle, self).copy(cr, uid, id, default, context=context)
380
381     def on_change_model(self, cr, uid, ids, model_id, context=None):
382         if not model_id:
383             return {}
384         model = self.pool.get('fleet.vehicle.model').browse(cr, uid, model_id, context=context)
385         return {
386             'value': {
387                 'image_medium': model.image,
388             }
389         }
390
391     def create(self, cr, uid, data, context=None):
392         #TODO: why is there a try..except? No idea, this is denis code, we should ask him
393         vehicle_id = super(fleet_vehicle, self).create(cr, uid, data, context=context)
394         try:
395             vehicle = self.browse(cr, uid, vehicle_id, context=context)
396             self.message_post(cr, uid, [vehicle_id], body=_('Vehicle %s has been added to the fleet!') % (vehicle.license_plate), context=context)
397         except:
398             pass # group deleted: do not push a message
399         return vehicle_id
400
401     def write(self, cr, uid, ids, vals, context=None):
402         #TODO: put comments
403         #TODO: use _() to translate labels
404         #TODO: why is there a try..except?
405         #TODO: shorten the code (e.g: oldmodel = vehicle.model_id and olmodel.model_id.name or _('None')
406         #TODO: in PEP 8 standard, a coma should be followed by a space, '+' operator and equal sign should be in between 2 spaces
407         #TODO: you're looping on `ids´, and in this loop you're writing again and posting logs on `ids´. Use message_post only on vehicle.id and put super write() outside of the loop.
408         for vehicle in self.browse(cr, uid, ids, context):
409             changes = []
410             if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
411                 value = self.pool.get('fleet.vehicle.model').browse(cr,uid,vals['model_id'],context=context).name
412                 oldmodel = vehicle.model_id
413                 if oldmodel:
414                     oldmodel = oldmodel.name
415                 else:
416                     oldmodel = 'None'
417                 changes.append(_('Model: from \' %s \' to \' %s \'') %(oldmodel, value))
418             if 'driver' in vals and vehicle.driver.id != vals['driver']:
419                 value = self.pool.get('res.partner').browse(cr,uid,vals['driver'],context=context).name
420                 olddriver = vehicle.driver
421                 if olddriver:
422                     olddriver = olddriver.name
423                 else:
424                     olddriver = 'None'
425                 changes.append(_('Driver: from \' %s \' to \' %s \'') %(olddriver, value))
426             if 'state' in vals and vehicle.state.id != vals['state']:
427                 value = self.pool.get('fleet.vehicle.state').browse(cr,uid,vals['state'],context=context).name
428                 oldstate = vehicle.state
429                 if oldstate:
430                     oldstate=oldstate.name
431                 else:
432                     oldstate = 'None'
433                 changes.append(_('State: from \' %s \' to \' %s \'') %(oldstate, value))
434             if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
435                 old_license_plate = vehicle.license_plate
436                 if not old_license_plate:
437                     old_license_plate = 'None'
438                 changes.append(_('License Plate: from \' %s \' to \' %s \'') %(old_license_plate, vals['license_plate']))
439            
440             try:
441                 if len(changes) > 0:
442                     self.message_post(cr, uid, [self.browse(cr, uid, vehicle.id, context)[0].id], body=", ".join(changes), context=context)
443             except Exception as e:
444                 print e
445                 pass
446
447         vehicle_id = super(fleet_vehicle,self).write(cr, uid, ids, vals, context)
448         return True
449
450
451 class fleet_vehicle_odometer(osv.Model):
452     _name='fleet.vehicle.odometer'
453     _description='Odometer log for a vehicle'
454     _order='date desc'
455
456     def _vehicle_log_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
457         res = {}
458         for record in self.browse(cr, uid, ids, context=context):
459             name = record.vehicle_id.name
460             if record.date:
461                 name = name+ ' / '+ str(record.date)
462             res[record.id] = name
463         return res
464
465     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
466         if not vehicle_id:
467             return {}
468         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
469         return {
470             'value': {
471                 'unit': odometer_unit,
472             }
473         }
474
475     _columns = {
476         'name': fields.function(_vehicle_log_name_get_fnc, type="char", string='Name', store=True),
477         'date': fields.date('Date'),
478         'value': fields.float('Odometer Value', group_operator="max"),
479         'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True),
480         'unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
481     }
482     _defaults = {
483         'date': fields.date.context_today,
484     }
485
486
487 class fleet_vehicle_log_fuel(osv.Model):
488
489     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
490         if not vehicle_id:
491             return {}
492         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
493         return {
494             'value': {
495                 'odometer_unit': odometer_unit,
496             }
497         }
498
499     def on_change_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
500         if liter > 0 and price_per_liter > 0:
501             return {'value' : {'amount' : liter * price_per_liter,}}
502         elif liter > 0 and amount > 0:
503             return {'value' : {'price_per_liter' : amount / liter,}}
504         elif price_per_liter > 0 and amount > 0:
505             return {'value' : {'liter' : amount / price_per_liter,}}
506         else :
507             return {}
508
509     def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
510
511         if price_per_liter > 0 and liter > 0:
512             return {'value' : {'amount' : liter * price_per_liter,}}
513         elif price_per_liter > 0 and amount > 0:
514             return {'value' : {'liter' : amount / price_per_liter,}}
515         elif liter > 0 and amount > 0:
516             return {'value' : {'price_per_liter' : amount / liter,}}
517         else :
518             return {}
519
520     def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
521
522         if amount > 0 and liter > 0:
523             return {'value': {'price_per_liter': amount / liter}}
524         elif amount > 0 and price_per_liter > 0:
525             return {'value': {'liter': amount / price_per_liter}}
526         elif liter > 0 and price_per_liter > 0:
527             return {'value': {'amount': liter * price_per_liter}}
528         return {}
529
530     def _get_default_service_type(self, cr, uid, context):
531         try:
532             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
533         except ValueError:
534             model_id = False
535         return model_id
536
537     _name = 'fleet.vehicle.log.fuel'
538     _description = 'Fuel log for vehicles'
539     _inherits = {'fleet.vehicle.cost': 'cost_id'}
540
541     _columns = {
542         'liter': fields.float('Liter'),
543         'price_per_liter': fields.float('Price Per Liter'),
544         'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
545         'inv_ref': fields.char('Invoice Reference', size=64),
546         'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
547         'notes': fields.text('Notes'),
548         'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
549     }
550     _defaults = {
551         'purchaser_id': lambda self, cr, uid, ctx: uid,
552         'date': fields.date.context_today,
553         'cost_subtype': _get_default_service_type,
554         'cost_type': 'fuel',
555     }
556
557
558 class fleet_vehicle_log_services(osv.Model):
559
560     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
561         if not vehicle_id:
562             return {}
563         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
564         return {
565             'value': {
566                 'odometer_unit': odometer_unit,
567             }
568         }
569
570     def _get_default_service_type(self, cr, uid, context):
571         try:
572             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
573         except ValueError:
574             model_id = False
575         return model_id
576
577     _inherits = {'fleet.vehicle.cost': 'cost_id'}
578     _name = 'fleet.vehicle.log.services'
579     _description = 'Services for vehicles'
580     _columns = {
581         'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
582         'inv_ref': fields.char('Invoice Reference', size=64),
583         'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
584         'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
585         'notes': fields.text('Notes'),
586     }
587     _defaults = {
588         'purchaser_id': lambda self, cr, uid, ctx: uid,
589         'date': fields.date.context_today,
590         'cost_subtype': _get_default_service_type,
591         'cost_type': 'services'
592     }
593
594
595 class fleet_service_type(osv.Model):
596     _name = 'fleet.service.type'
597     _description = 'Type of services available on a vehicle'
598     _columns = {
599         'name': fields.char('Name', required=True, translate=True),
600         'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
601     }
602
603
604 class fleet_vehicle_log_contract(osv.Model):
605
606     def run_scheduler(self,cr,uid,context=None):
607         #TODO: add comments, will ask denis, this is his code
608         vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
609         d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
610         contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
611         deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
612         for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
613             if not contract.start_date or contract.cost_frequency == 'no':
614                 continue
615             found = False
616             last_cost_date = contract.start_date
617             if contract.generated_cost_ids:
618                 last_autogenerated_cost_id = vehicle_cost_obj.search(cr, uid, ['&', ('contract_id','=',contract.id), ('auto_generated','=',True)], offset=0, limit=1, order='date desc',context=context, count=False)
619                 if last_autogenerated_cost_id:
620                     found = True
621                     last_cost_date = vehicle_cost_obj.browse(cr, uid, last_cost_id[0], context=context).date
622             startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
623             if found:
624                 startdate += deltas.get(contract.cost_frequency)
625             while (startdate < d) & (startdate < datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
626                 data = {
627                     'amount': contract.cost_generated,
628                     'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
629                     'vehicle_id': contract.vehicle_id.id,
630                     'cost_subtype': contract.cost_subtype.id,
631                     'contract_id': contract.id,
632                     'auto_generated': True
633                 }
634                 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
635                 startdate += deltas.get(contract.cost_frequency)
636
637         #for some reason when 2 scheduler run at the same time on the cron, we have a deadlock, so we only run it once and we call the other
638         #function here.
639         self.pool.get('fleet.vehicle').run_scheduler(cr,uid,context=context)
640
641         return True
642
643     def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
644         res = {}
645         for record in self.browse(cr, uid, ids, context=context):
646             name = record.vehicle_id.name
647             if record.cost_subtype.name:
648                 name += ' / '+ record.cost_subtype.name
649             if record.date:
650                 name += ' / '+ record.date
651             res[record.id] = name
652         return res
653
654     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
655         if not vehicle_id:
656             return {}
657         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
658         return {
659             'value': {
660                 'odometer_unit': odometer_unit,
661             }
662         }
663
664     def compute_next_year_date(self, strdate):
665         oneyear = datetime.timedelta(days=365)
666         curdate = str_to_date(strdate)
667         return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
668
669     def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
670         if (strdate):
671             return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
672         return {}
673
674     def get_days_left(self,cr,uid,ids,prop,unknow_none,context=None):
675         """return a dict with as value for each contract an integer
676         if contract is in an open state and is overdue, return 0
677         if contract is in a closed state, return -1
678         otherwise return the number of days before the contract expires
679         """
680         reads = self.browse(cr,uid,ids,context=context)
681         res={}
682         for record in reads:
683             if (record.expiration_date and (record.state=='open' or record.state=='toclose')):
684                 today= str_to_date(time.strftime('%Y-%m-%d'))
685                 renew_date = str_to_date(record.expiration_date)
686                 diff_time=int((renew_date-today).days)
687                 if (diff_time<=0):
688                     res[record.id]=0
689                 else:
690                     res[record.id]=diff_time
691             else:
692                 res[record.id]=-1
693         return res
694
695     def act_renew_contract(self,cr,uid,ids,context=None):
696         default={}
697         contracts = self.browse(cr,uid,ids,context=context)
698         for element in contracts:
699             default['date']=fields.date.context_today(self, cr, uid, context=context)
700             default['start_date']=str(str_to_date(element.expiration_date)+datetime.timedelta(days=1))
701             #compute end date
702             startdate = str_to_date(element.start_date)
703             enddate = str_to_date(element.expiration_date)
704             diffdate = (enddate-startdate)
705             newenddate = enddate+diffdate
706             default['expiration_date']=str(newenddate)
707         
708         newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, ids[0], default, context=context)
709         mod,modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
710         return {
711             'name':_("Renew Contract"),
712             'view_mode': 'form',
713             'view_id': modid,
714             'view_type': 'tree,form',
715             'res_model': 'fleet.vehicle.log.contract',
716             'type': 'ir.actions.act_window',
717             'nodestroy': True,
718             'domain': '[]',
719             'res_id': newid,
720             'context': {'active_id':newid}, 
721         }
722
723     def _get_default_contract_type(self, cr, uid, context=None):
724         try:
725             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
726         except ValueError:
727             model_id = False
728         return model_id
729
730     def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
731         totalsum = 0.0
732         for element in cost_ids:
733             if element and len(element) == 3 and element[2] is not False:
734                 totalsum += element[2].get('amount', 0.0)
735         return {
736             'value': {
737                 'sum_cost': totalsum,
738             }
739         }
740
741     def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
742         res = {}
743         for contract in self.browse(cr, uid, ids, context=context):
744             totalsum = 0
745             for cost in contract.cost_ids:
746                 totalsum += cost.amount
747             res[contract.id] = totalsum
748         return res
749
750     _inherits = {'fleet.vehicle.cost': 'cost_id'}
751     _name = 'fleet.vehicle.log.contract'
752     _description = 'Contract information on a vehicle'
753     _order='state desc,expiration_date'
754     _columns = {
755         'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
756         'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
757         'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
758         'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
759         'insurer_id' :fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
760         'purchaser_id': fields.many2one('res.partner', 'Contractor', domain="['|', ('customer','=',True), ('employee','=',True)]",help='Person to which the contract is signed for'),
761         'ins_ref': fields.char('Contract Reference', size=64),
762         'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')], 'Status', readonly=True, help='Choose wheter the contract is still valid or not'),
763         'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract'),
764         'cost_generated': fields.float('Recurring Cost Amount', help="Costs paid at regular intervals, depending on the cost frequency. If the cost frequency is set to unique, the cost will be logged at the start date"),
765         'cost_frequency': fields.selection([('no','No'), ('daily', 'Daily'), ('weekly','Weekly'), ('monthly','Monthly'), ('yearly','Yearly')], 'Recurring Cost Frequency', help='Frequency of the recuring cost', required=True),
766         'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs', ondelete='cascade'),
767         'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
768         'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
769     }
770     _defaults = {
771         'purchaser_id': lambda self, cr, uid, ctx: uid,
772         'date': fields.date.context_today,
773         'start_date': fields.date.context_today,
774         'state':'open',
775         'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
776         'cost_frequency': 'no',
777         'cost_subtype': _get_default_contract_type,
778         'cost_type': 'contract',
779     }
780
781     def copy(self, cr, uid, id, default=None, context=None):
782         if default is None:
783             default = {}
784         today = fields.date.context_today(self, cr, uid, context=context)
785         default['date'] = today
786         default['start_date'] = today
787         default['expiration_date'] = self.compute_next_year_date(today)
788         default['ins_ref'] = ''
789         default['state'] = 'open'
790         default['notes'] = ''
791         return super(fleet_vehicle_log_contract, self).copy(cr, uid, id, default, context=context)
792
793     def contract_close(self, cr, uid, ids, context=None):
794         return self.write(cr, uid, ids, {'state': 'closed'},context=context)
795
796     def contract_open(self, cr, uid, ids, context=None):
797         return self.write(cr, uid, ids, {'state': 'open'},context=context)
798
799 class fleet_contract_state(osv.Model):
800     _name = 'fleet.contract.state'
801     _description = 'Contains the different possible status of a leasing contract'
802
803     _columns = {
804         'name':fields.char('Contract Status', size=32, required=True),
805     }