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