[REF]refactoring of search function to find vehicle with due or overdue contracts
[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)
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         if 'parent_id' in data and data['parent_id']:
96             parent = self.browse(cr, uid, data['parent_id'], context=context)
97             data['vehicle_id'] = parent.vehicle_id.id
98             data['date'] = parent.date
99             data['cost_type'] = parent.cost_type
100         if 'contract_id' in data and data['contract_id']:
101             contract = self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, data['contract_id'], context=context)
102             data['vehicle_id'] = contract.vehicle_id.id
103             data['cost_subtype'] = contract.cost_subtype.id
104             data['cost_type'] = contract.cost_type
105         if 'odometer' in data and not data['odometer']:
106             del(data['odometer'])
107         return super(fleet_vehicle_cost, self).create(cr, uid, data, context=context)
108
109
110 class fleet_vehicle_tag(osv.Model):
111     _name = 'fleet.vehicle.tag'
112     _columns = {
113         'name': fields.char('Name', required=True, translate=True),
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             assert operator == '>' and value == 0, 'Operation not supported'
252             today = fields.date.context_today(self, cr, uid, context=context)
253             cr.execute('select cost.vehicle_id, count(contract.id) as contract_number FROM fleet_vehicle_cost cost left join fleet_vehicle_log_contract contract on contract.cost_id = cost.id WHERE contract.expiration_date is not null AND contract.expiration_date < %s AND contract.state IN (\'open\', \'toclose\') GROUP BY cost.vehicle_id', (today,))
254             res_ids = [x[0] for x in cr.fetchall()]
255             res.append(('id', 'in', res_ids))      
256         return res
257     
258     def _search_contract_renewal_due_soon(self, cr, uid, obj, name, args, context):
259         res = []
260         for field, operator, value in args:
261             assert operator == '>' and value == 0, 'Operation not supported'
262             today = fields.date.context_today(self, cr, uid, context=context)
263             datetime_today = datetime.datetime.strptime(today, tools.DEFAULT_SERVER_DATE_FORMAT)
264             limit_date = str((datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
265             cr.execute('select cost.vehicle_id, count(contract.id) as contract_number FROM fleet_vehicle_cost cost left join fleet_vehicle_log_contract contract on contract.cost_id = cost.id WHERE contract.expiration_date is not null AND contract.expiration_date > %s AND contract.expiration_date < %s AND contract.state IN (\'open\', \'toclose\') GROUP BY cost.vehicle_id', (today, limit_date))
266             res_ids = [x[0] for x in cr.fetchall()]
267             res.append(('id', 'in', res_ids))    
268         return res
269
270     def _get_contract_reminder_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
271         res= {}
272         for record in self.browse(cr, uid, ids, context=context):
273             overdue = 0
274             due_soon = 0
275             name = ''
276             for element in record.log_contracts:
277                 if (element.state in ('open', 'toclose') and element.expiration_date):
278                     current_date_str = fields.date.context_today(self, cr, uid, context=context)
279                     due_time_str = element.expiration_date
280                     current_date = str_to_date(current_date_str)
281                     due_time = str_to_date(due_time_str)
282                     diff_time = (due_time-current_date).days
283                     if diff_time < 0:
284                         overdue += 1
285                     if diff_time<15 and diff_time>=0:
286                             due_soon = due_soon +1;
287                     if overdue+due_soon>0:
288                         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')
289                         if len(ids) > 0:
290                             name=(self.pool.get('fleet.vehicle.log.contract').browse(cr,uid,ids[0],context=context).cost_subtype.name)
291
292             res[record.id] = {
293                 'contract_renewal_overdue':overdue,
294                 'contract_renewal_due_soon':due_soon,
295                 'contract_renewal_total':(overdue+due_soon-1),
296                 'contract_renewal_name':name,
297             }
298         return res
299
300     def run_scheduler(self, cr, uid, context=None):
301         datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
302         limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
303         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)
304         res = {}
305         for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, ids, context=context):
306             if contract.vehicle_id.id in res:
307                 res[contract.vehicle_id.id] += 1
308             else:
309                 res[contract.vehicle_id.id] = 1
310
311         for vehicle, value in res.items():
312             self.message_post(cr, uid, vehicle, body=_('%s contract(s) need(s) to be renewed and/or closed!') % (str(value)), context=context)
313
314         return self.pool.get('fleet.vehicle.log.contract').write(cr, uid, ids, {'state': 'toclose'}, context=context)
315
316     def _get_default_state(self, cr, uid, context):
317         try:
318             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'vehicle_state_active')
319         except ValueError:
320             model_id = False
321         return model_id
322
323     _name = 'fleet.vehicle'
324     _description = 'Information on a vehicle'
325     _order= 'license_plate asc'
326     _columns = {
327         'name': fields.function(_vehicle_name_get_fnc, type="char", string='Name', store=True),
328         'company_id': fields.many2one('res.company', 'Company'),
329         'license_plate': fields.char('License Plate', size=32, required=True, help='License plate number of the vehicle (ie: plate number for a car)'),
330         'vin_sn': fields.char('Chassis Number', size=32, help='Unique number written on the vehicle motor (VIN/SN number)'),
331         'driver': fields.many2one('res.partner', 'Driver', help='Driver of the vehicle'),
332         'model_id': fields.many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle'),
333         'log_fuel': fields.one2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs'),
334         'log_services': fields.one2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs'),
335         'log_contracts': fields.one2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts'),
336         'acquisition_date': fields.date('Acquisition Date', required=False, help='Date when the vehicle has been bought'),
337         'color': fields.char('Color', size=32, help='Color of the vehicle'),
338         'state': fields.many2one('fleet.vehicle.state', 'State', help='Current state of the vehicle', ondelete="set null"),
339         'location': fields.char('Location', size=128, help='Location of the vehicle (garage, ...)'),
340         'seats': fields.integer('Seats Number', help='Number of seats of the vehicle'),
341         'doors': fields.integer('Doors Number', help='Number of doors of the vehicle'),
342         'tag_ids' :fields.many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id','tag_id', 'Tags'),
343         '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'),
344         'odometer_unit': fields.selection([('kilometers', 'Kilometers'),('miles','Miles')], 'Odometer Unit', help='Unit of the odometer ',required=True),
345         'transmission': fields.selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle'),
346         'fuel_type': fields.selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle'),
347         'horsepower': fields.integer('Horsepower'),
348         'horsepower_tax': fields.float('Horsepower Taxation'),
349         'power': fields.integer('Power (kW)', help='Power in kW of the vehicle'),
350         'co2': fields.float('CO2 Emissions', help='CO2 emissions of the vehicle'),
351         'image': fields.related('model_id', 'image', type="binary", string="Logo"),
352         'image_medium': fields.related('model_id', 'image_medium', type="binary", string="Logo"),
353         'image_small': fields.related('model_id', 'image_small', type="binary", string="Logo"),
354         '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'),
355         'contract_renewal_overdue': fields.function(_get_contract_reminder_fnc, fnct_search=_search_get_overdue_contract_reminder, type="integer", string='Contracts Overdued', multi='contract_info'),
356         'contract_renewal_name': fields.function(_get_contract_reminder_fnc, type="text", string='Name of contract to renew soon', multi='contract_info'),
357         'contract_renewal_total': fields.function(_get_contract_reminder_fnc, type="integer", string='Total of contracts due or overdue minus one', multi='contract_info'),
358         'car_value': fields.float('Car Value', help='Value of the bought vehicle'),
359         }
360
361     _defaults = {
362         'doors': 5,
363         'odometer_unit': 'kilometers',
364         'state': _get_default_state,
365     }
366
367     def copy(self, cr, uid, id, default=None, context=None):
368         if not default:
369             default = {}
370         default.update({
371             'log_fuel':[],
372             'log_contracts':[],
373             'log_services':[],
374             'tag_ids':[],
375             'vin_sn':'',
376         })
377         return super(fleet_vehicle, self).copy(cr, uid, id, default, context=context)
378
379     def on_change_model(self, cr, uid, ids, model_id, context=None):
380         if not model_id:
381             return {}
382         model = self.pool.get('fleet.vehicle.model').browse(cr, uid, model_id, context=context)
383         return {
384             'value': {
385                 'image_medium': model.image,
386             }
387         }
388
389     def create(self, cr, uid, data, context=None):
390         vehicle_id = super(fleet_vehicle, self).create(cr, uid, data, context=context)
391         vehicle = self.browse(cr, uid, vehicle_id, context=context)
392         self.message_post(cr, uid, [vehicle_id], body=_('Vehicle %s has been added to the fleet!') % (vehicle.license_plate), context=context)
393         return vehicle_id
394
395     def write(self, cr, uid, ids, vals, context=None):
396         """
397         This function write an entry in the openchatter whenever we change important information
398         on the vehicle like the model, the drive, the state of the vehicle or its license plate
399         """
400         for vehicle in self.browse(cr, uid, ids, context):
401             changes = []
402             if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
403                 value = self.pool.get('fleet.vehicle.model').browse(cr,uid,vals['model_id'],context=context).name
404                 oldmodel = vehicle.model_id.name or _('None')
405                 changes.append(_('Model: from \' %s \' to \' %s \'') %(oldmodel, value))
406             if 'driver' in vals and vehicle.driver.id != vals['driver']:
407                 value = self.pool.get('res.partner').browse(cr,uid,vals['driver'],context=context).name
408                 olddriver = (vehicle.driver.name) or _('None')
409                 changes.append(_('Driver: from \' %s \' to \' %s \'') %(olddriver, value))
410             if 'state' in vals and vehicle.state.id != vals['state']:
411                 value = self.pool.get('fleet.vehicle.state').browse(cr,uid,vals['state'],context=context).name
412                 oldstate = vehicle.state.name or _('None')
413                 changes.append(_('State: from \' %s \' to \' %s \'') %(oldstate, value))
414             if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
415                 old_license_plate = vehicle.license_plate or _('None')
416                 changes.append(_('License Plate: from \' %s \' to \' %s \'') %(old_license_plate, vals['license_plate']))
417            
418             if len(changes) > 0:
419                 self.message_post(cr, uid, [vehicle.id], body=", ".join(changes), context=context)
420
421         vehicle_id = super(fleet_vehicle,self).write(cr, uid, ids, vals, context)
422         return True
423
424
425 class fleet_vehicle_odometer(osv.Model):
426     _name='fleet.vehicle.odometer'
427     _description='Odometer log for a vehicle'
428     _order='date desc'
429
430     def _vehicle_log_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
431         res = {}
432         for record in self.browse(cr, uid, ids, context=context):
433             name = record.vehicle_id.name
434             if record.date:
435                 name = name+ ' / '+ str(record.date)
436             res[record.id] = name
437         return res
438
439     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
440         if not vehicle_id:
441             return {}
442         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
443         return {
444             'value': {
445                 'unit': odometer_unit,
446             }
447         }
448
449     _columns = {
450         'name': fields.function(_vehicle_log_name_get_fnc, type="char", string='Name', store=True),
451         'date': fields.date('Date'),
452         'value': fields.float('Odometer Value', group_operator="max"),
453         'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True),
454         'unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
455     }
456     _defaults = {
457         'date': fields.date.context_today,
458     }
459
460
461 class fleet_vehicle_log_fuel(osv.Model):
462
463     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
464         if not vehicle_id:
465             return {}
466         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
467         return {
468             'value': {
469                 'odometer_unit': odometer_unit,
470             }
471         }
472
473     def on_change_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
474         if liter > 0 and price_per_liter > 0:
475             return {'value' : {'amount' : liter * price_per_liter,}}
476         elif liter > 0 and amount > 0:
477             return {'value' : {'price_per_liter' : amount / liter,}}
478         elif price_per_liter > 0 and amount > 0:
479             return {'value' : {'liter' : amount / price_per_liter,}}
480         else :
481             return {}
482
483     def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
484
485         if price_per_liter > 0 and liter > 0:
486             return {'value' : {'amount' : liter * price_per_liter,}}
487         elif price_per_liter > 0 and amount > 0:
488             return {'value' : {'liter' : amount / price_per_liter,}}
489         elif liter > 0 and amount > 0:
490             return {'value' : {'price_per_liter' : amount / liter,}}
491         else :
492             return {}
493
494     def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
495
496         if amount > 0 and liter > 0:
497             return {'value': {'price_per_liter': amount / liter}}
498         elif amount > 0 and price_per_liter > 0:
499             return {'value': {'liter': amount / price_per_liter}}
500         elif liter > 0 and price_per_liter > 0:
501             return {'value': {'amount': liter * price_per_liter}}
502         return {}
503
504     def _get_default_service_type(self, cr, uid, context):
505         try:
506             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
507         except ValueError:
508             model_id = False
509         return model_id
510
511     _name = 'fleet.vehicle.log.fuel'
512     _description = 'Fuel log for vehicles'
513     _inherits = {'fleet.vehicle.cost': 'cost_id'}
514
515     _columns = {
516         'liter': fields.float('Liter'),
517         'price_per_liter': fields.float('Price Per Liter'),
518         'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
519         'inv_ref': fields.char('Invoice Reference', size=64),
520         'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
521         'notes': fields.text('Notes'),
522         '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
523     }
524     _defaults = {
525         'purchaser_id': lambda self, cr, uid, ctx: uid,
526         'date': fields.date.context_today,
527         'cost_subtype': _get_default_service_type,
528         'cost_type': 'fuel',
529     }
530
531
532 class fleet_vehicle_log_services(osv.Model):
533
534     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
535         if not vehicle_id:
536             return {}
537         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
538         return {
539             'value': {
540                 'odometer_unit': odometer_unit,
541             }
542         }
543
544     def _get_default_service_type(self, cr, uid, context):
545         try:
546             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
547         except ValueError:
548             model_id = False
549         return model_id
550
551     _inherits = {'fleet.vehicle.cost': 'cost_id'}
552     _name = 'fleet.vehicle.log.services'
553     _description = 'Services for vehicles'
554     _columns = {
555         'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
556         'inv_ref': fields.char('Invoice Reference', size=64),
557         'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
558         '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
559         'notes': fields.text('Notes'),
560     }
561     _defaults = {
562         'purchaser_id': lambda self, cr, uid, ctx: uid,
563         'date': fields.date.context_today,
564         'cost_subtype': _get_default_service_type,
565         'cost_type': 'services'
566     }
567
568
569 class fleet_service_type(osv.Model):
570     _name = 'fleet.service.type'
571     _description = 'Type of services available on a vehicle'
572     _columns = {
573         'name': fields.char('Name', required=True, translate=True),
574         'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
575     }
576
577
578 class fleet_vehicle_log_contract(osv.Model):
579
580     def run_scheduler(self,cr,uid,context=None):
581         #This method is called by a cron task
582         #It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
583         #For example, if a contract has a reccuring cost of 200 with a weekly frequency, this method creates a cost of 200 on the first day of each week, from the date of the last recurring costs in the database to today
584         #If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
585         #The created costs are associated to a contract thanks to the many2one field contract_id
586         #If the contract has no start_date, no cost will be created, even if the contract has recurring costs
587         vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
588         d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
589         contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
590         deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
591         for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
592             if not contract.start_date or contract.cost_frequency == 'no':
593                 continue
594             found = False
595             last_cost_date = contract.start_date
596             if contract.generated_cost_ids:
597                 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)
598                 if last_autogenerated_cost_id:
599                     found = True
600                     last_cost_date = vehicle_cost_obj.browse(cr, uid, last_cost_id[0], context=context).date
601             startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
602             if found:
603                 startdate += deltas.get(contract.cost_frequency)
604             while (startdate < d) & (startdate < datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
605                 data = {
606                     'amount': contract.cost_generated,
607                     'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
608                     'vehicle_id': contract.vehicle_id.id,
609                     'cost_subtype': contract.cost_subtype.id,
610                     'contract_id': contract.id,
611                     'auto_generated': True
612                 }
613                 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
614                 startdate += deltas.get(contract.cost_frequency)
615
616         #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
617         #function here.
618         self.pool.get('fleet.vehicle').run_scheduler(cr,uid,context=context)
619
620         return True
621
622     def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
623         res = {}
624         for record in self.browse(cr, uid, ids, context=context):
625             name = record.vehicle_id.name
626             if record.cost_subtype.name:
627                 name += ' / '+ record.cost_subtype.name
628             if record.date:
629                 name += ' / '+ record.date
630             res[record.id] = name
631         return res
632
633     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
634         if not vehicle_id:
635             return {}
636         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
637         return {
638             'value': {
639                 'odometer_unit': odometer_unit,
640             }
641         }
642
643     def compute_next_year_date(self, strdate):
644         oneyear = datetime.timedelta(days=365)
645         curdate = str_to_date(strdate)
646         return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
647
648     def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
649         if (strdate):
650             return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
651         return {}
652
653     def get_days_left(self,cr,uid,ids,prop,unknow_none,context=None):
654         """return a dict with as value for each contract an integer
655         if contract is in an open state and is overdue, return 0
656         if contract is in a closed state, return -1
657         otherwise return the number of days before the contract expires
658         """
659         reads = self.browse(cr,uid,ids,context=context)
660         res={}
661         for record in reads:
662             if (record.expiration_date and (record.state=='open' or record.state=='toclose')):
663                 today= str_to_date(time.strftime('%Y-%m-%d'))
664                 renew_date = str_to_date(record.expiration_date)
665                 diff_time=int((renew_date-today).days)
666                 if (diff_time<=0):
667                     res[record.id]=0
668                 else:
669                     res[record.id]=diff_time
670             else:
671                 res[record.id]=-1
672         return res
673
674     def act_renew_contract(self,cr,uid,ids,context=None):
675         default={}
676         contracts = self.browse(cr,uid,ids,context=context)
677         for element in contracts:
678             default['date']=fields.date.context_today(self, cr, uid, context=context)
679             default['start_date']=str(str_to_date(element.expiration_date)+datetime.timedelta(days=1))
680             #compute end date
681             startdate = str_to_date(element.start_date)
682             enddate = str_to_date(element.expiration_date)
683             diffdate = (enddate-startdate)
684             newenddate = enddate+diffdate
685             default['expiration_date']=str(newenddate)
686         
687         newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, ids[0], default, context=context)
688         mod,modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
689         return {
690             'name':_("Renew Contract"),
691             'view_mode': 'form',
692             'view_id': modid,
693             'view_type': 'tree,form',
694             'res_model': 'fleet.vehicle.log.contract',
695             'type': 'ir.actions.act_window',
696             'nodestroy': True,
697             'domain': '[]',
698             'res_id': newid,
699             'context': {'active_id':newid}, 
700         }
701
702     def _get_default_contract_type(self, cr, uid, context=None):
703         try:
704             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
705         except ValueError:
706             model_id = False
707         return model_id
708
709     def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
710         totalsum = 0.0
711         for element in cost_ids:
712             if element and len(element) == 3 and element[2] is not False:
713                 totalsum += element[2].get('amount', 0.0)
714         return {
715             'value': {
716                 'sum_cost': totalsum,
717             }
718         }
719
720     def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
721         res = {}
722         for contract in self.browse(cr, uid, ids, context=context):
723             totalsum = 0
724             for cost in contract.cost_ids:
725                 totalsum += cost.amount
726             res[contract.id] = totalsum
727         return res
728
729     _inherits = {'fleet.vehicle.cost': 'cost_id'}
730     _name = 'fleet.vehicle.log.contract'
731     _description = 'Contract information on a vehicle'
732     _order='state desc,expiration_date'
733     _columns = {
734         'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
735         'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
736         'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
737         'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
738         'insurer_id' :fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
739         'purchaser_id': fields.many2one('res.partner', 'Contractor', domain="['|', ('customer','=',True), ('employee','=',True)]",help='Person to which the contract is signed for'),
740         'ins_ref': fields.char('Contract Reference', size=64),
741         'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')], 'Status', readonly=True, help='Choose wheter the contract is still valid or not'),
742         'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract'),
743         '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"),
744         '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),
745         'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs', ondelete='cascade'),
746         'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
747         '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
748     }
749     _defaults = {
750         'purchaser_id': lambda self, cr, uid, ctx: uid,
751         'date': fields.date.context_today,
752         'start_date': fields.date.context_today,
753         'state':'open',
754         'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
755         'cost_frequency': 'no',
756         'cost_subtype': _get_default_contract_type,
757         'cost_type': 'contract',
758     }
759
760     def copy(self, cr, uid, id, default=None, context=None):
761         if default is None:
762             default = {}
763         today = fields.date.context_today(self, cr, uid, context=context)
764         default['date'] = today
765         default['start_date'] = today
766         default['expiration_date'] = self.compute_next_year_date(today)
767         default['ins_ref'] = ''
768         default['state'] = 'open'
769         default['notes'] = ''
770         return super(fleet_vehicle_log_contract, self).copy(cr, uid, id, default, context=context)
771
772     def contract_close(self, cr, uid, ids, context=None):
773         return self.write(cr, uid, ids, {'state': 'closed'},context=context)
774
775     def contract_open(self, cr, uid, ids, context=None):
776         return self.write(cr, uid, ids, {'state': 'open'},context=context)
777
778 class fleet_contract_state(osv.Model):
779     _name = 'fleet.contract.state'
780     _description = 'Contains the different possible status of a leasing contract'
781
782     _columns = {
783         'name':fields.char('Contract Status', size=32, required=True),
784     }