[MERGE] Sync with trunk
[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 openerp.osv import fields, osv
23 import time
24 import datetime
25 from openerp import tools
26 from openerp.osv.orm import except_orm
27 from openerp.tools.translate import _
28 from dateutil.relativedelta import relativedelta
29
30 def str_to_datetime(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             if (record.date):
60                 res[record.id] = str(time.strptime(record.date, tools.DEFAULT_SERVER_DATE_FORMAT).tm_year)
61             else:
62                 res[record.id] = _('Unknown')
63         return res
64
65     def _cost_name_get_fnc(self, cr, uid, ids, name, unknow_none, context=None):
66         res = {}
67         for record in self.browse(cr, uid, ids, context=context):
68             name = record.vehicle_id.name
69             if record.cost_subtype_id.name:
70                 name += ' / '+ record.cost_subtype_id.name
71             if record.date:
72                 name += ' / '+ record.date
73             res[record.id] = name
74         return res
75
76     _columns = {
77         'name': fields.function(_cost_name_get_fnc, type="char", string='Name', store=True),
78         'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True, help='Vehicle concerned by this log'),
79         'cost_subtype_id': fields.many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost'),
80         'amount': fields.float('Total Price'),
81         'cost_type': fields.selection([('contract', 'Contract'), ('services','Services'), ('fuel','Fuel'), ('other','Other')], 'Category of the cost', help='For internal purpose only', required=True),
82         'parent_id': fields.many2one('fleet.vehicle.cost', 'Parent', help='Parent cost to this current cost'),
83         'cost_ids': fields.one2many('fleet.vehicle.cost', 'parent_id', 'Included Services'),
84         'odometer_id': fields.many2one('fleet.vehicle.odometer', 'Odometer', help='Odometer measure of the vehicle at the moment of this log'),
85         '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'),
86         'odometer_unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
87         'date' :fields.date('Date',help='Date when the cost has been executed'),
88         'contract_id': fields.many2one('fleet.vehicle.log.contract', 'Contract', help='Contract attached to this cost'),
89         'auto_generated': fields.boolean('Automatically Generated', readonly=True, required=True),
90         'year': fields.function(_year_get_fnc, type="char", string='Year', store=True),
91     }
92
93     _defaults ={
94         'cost_type': 'other',
95     }
96
97     def create(self, cr, uid, data, context=None):
98         #make sure that the data are consistent with values of parent and contract records given
99         if 'parent_id' in data and data['parent_id']:
100             parent = self.browse(cr, uid, data['parent_id'], context=context)
101             data['vehicle_id'] = parent.vehicle_id.id
102             data['date'] = parent.date
103             data['cost_type'] = parent.cost_type
104         if 'contract_id' in data and data['contract_id']:
105             contract = self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, data['contract_id'], context=context)
106             data['vehicle_id'] = contract.vehicle_id.id
107             data['cost_subtype_id'] = contract.cost_subtype_id.id
108             data['cost_type'] = contract.cost_type
109         if 'odometer' in data and not data['odometer']:
110             #if received value for odometer is 0, then remove it from the data as it would result to the creation of a
111             #odometer log with 0, which is to be avoided
112             del(data['odometer'])
113         return super(fleet_vehicle_cost, self).create(cr, uid, data, context=context)
114
115
116 class fleet_vehicle_tag(osv.Model):
117     _name = 'fleet.vehicle.tag'
118     _columns = {
119         'name': fields.char('Name', required=True, translate=True),
120     }
121
122 class fleet_vehicle_state(osv.Model):
123     _name = 'fleet.vehicle.state'
124     _order = 'sequence asc'
125     _columns = {
126         'name': fields.char('Name', required=True),
127         'sequence': fields.integer('Sequence', help="Used to order the note stages")
128     }
129     _sql_constraints = [('fleet_state_name_unique','unique(name)', 'State name already exists')]
130
131
132 class fleet_vehicle_model(osv.Model):
133
134     def _model_name_get_fnc(self, cr, uid, ids, field_name, arg, context=None):
135         res = {}
136         for record in self.browse(cr, uid, ids, context=context):
137             name = record.modelname
138             if record.brand_id.name:
139                 name = record.brand_id.name + ' / ' + name
140             res[record.id] = name
141         return res
142
143     def on_change_brand(self, cr, uid, ids, model_id, context=None):
144         if not model_id:
145             return {'value': {'image_medium': False}}
146         brand = self.pool.get('fleet.vehicle.model.brand').browse(cr, uid, model_id, context=context)
147         return {
148             'value': {
149                 'image_medium': brand.image,
150             }
151         }
152
153     _name = 'fleet.vehicle.model'
154     _description = 'Model of a vehicle'
155     _order = 'name asc'
156
157     _columns = {
158         'name': fields.function(_model_name_get_fnc, type="char", string='Name', store=True),
159         'modelname': fields.char('Model name', size=32, required=True), 
160         'brand_id': fields.many2one('fleet.vehicle.model.brand', 'Model Brand', required=True, help='Brand of the vehicle'),
161         'vendors': fields.many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors'),
162         'image': fields.related('brand_id', 'image', type="binary", string="Logo"),
163         'image_medium': fields.related('brand_id', 'image_medium', type="binary", string="Logo"),
164         'image_small': fields.related('brand_id', 'image_small', type="binary", string="Logo"),
165     }
166
167
168 class fleet_vehicle_model_brand(osv.Model):
169     _name = 'fleet.vehicle.model.brand'
170     _description = 'Brand model of the vehicle'
171
172     _order = 'name asc'
173
174     def _get_image(self, cr, uid, ids, name, args, context=None):
175         result = dict.fromkeys(ids, False)
176         for obj in self.browse(cr, uid, ids, context=context):
177             result[obj.id] = tools.image_get_resized_images(obj.image)
178         return result
179
180     def _set_image(self, cr, uid, id, name, value, args, context=None):
181         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
182
183     _columns = {
184         'name': fields.char('Brand Name', size=64, required=True),
185         'image': fields.binary("Logo",
186             help="This field holds the image used as logo for the brand, limited to 1024x1024px."),
187         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
188             string="Medium-sized photo", type="binary", multi="_get_image",
189             store = {
190                 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
191             },
192             help="Medium-sized logo of the brand. It is automatically "\
193                  "resized as a 128x128px image, with aspect ratio preserved. "\
194                  "Use this field in form views or some kanban views."),
195         'image_small': fields.function(_get_image, fnct_inv=_set_image,
196             string="Smal-sized photo", type="binary", multi="_get_image",
197             store = {
198                 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
199             },
200             help="Small-sized photo of the brand. It is automatically "\
201                  "resized as a 64x64px image, with aspect ratio preserved. "\
202                  "Use this field anywhere a small image is required."),
203     }
204
205
206 class fleet_vehicle(osv.Model):
207
208     _inherit = 'mail.thread'
209
210     def _vehicle_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
211         res = {}
212         for record in self.browse(cr, uid, ids, context=context):
213             res[record.id] = record.model_id.brand_id.name + '/' + record.model_id.modelname + ' / ' + record.license_plate
214         return res
215
216     def return_action_to_open(self, cr, uid, ids, context=None):
217         """ This opens the xml view specified in xml_id for the current vehicle """
218         if context is None:
219             context = {}
220         if context.get('xml_id'):
221             res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet', context['xml_id'], context=context)
222             res['context'] = context
223             res['context'].update({'default_vehicle_id': ids[0]})
224             res['domain'] = [('vehicle_id','=', ids[0])]
225             return res
226         return False
227
228     def act_show_log_cost(self, cr, uid, ids, context=None):
229         """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
230             @return: the costs log view
231         """
232         if context is None:
233             context = {}
234         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet','fleet_vehicle_costs_act', context=context)
235         res['context'] = context
236         res['context'].update({
237             'default_vehicle_id': ids[0],
238             'search_default_parent_false': True
239         })
240         res['domain'] = [('vehicle_id','=', ids[0])]
241         return res
242
243     def _get_odometer(self, cr, uid, ids, odometer_id, arg, context):
244         res = dict.fromkeys(ids, 0)
245         for record in self.browse(cr,uid,ids,context=context):
246             ids = self.pool.get('fleet.vehicle.odometer').search(cr, uid, [('vehicle_id', '=', record.id)], limit=1, order='value desc')
247             if len(ids) > 0:
248                 res[record.id] = self.pool.get('fleet.vehicle.odometer').browse(cr, uid, ids[0], context=context).value
249         return res
250
251     def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
252         if value:
253             date = fields.date.context_today(self, cr, uid, context=context)
254             data = {'value': value, 'date': date, 'vehicle_id': id}
255             return self.pool.get('fleet.vehicle.odometer').create(cr, uid, data, context=context)
256
257     def _search_get_overdue_contract_reminder(self, cr, uid, obj, name, args, context):
258         res = []
259         for field, operator, value in args:
260             assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
261             if (operator == '=' and value == True) or (operator in ('<>', '!=') and value == False):
262                 search_operator = 'in'
263             else:
264                 search_operator = 'not in'
265             today = fields.date.context_today(self, cr, uid, context=context)
266             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,))
267             res_ids = [x[0] for x in cr.fetchall()]
268             res.append(('id', search_operator, res_ids))
269         return res
270
271     def _search_contract_renewal_due_soon(self, cr, uid, obj, name, args, context):
272         res = []
273         for field, operator, value in args:
274             assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
275             if (operator == '=' and value == True) or (operator in ('<>', '!=') and value == False):
276                 search_operator = 'in'
277             else:
278                 search_operator = 'not in'
279             today = fields.date.context_today(self, cr, uid, context=context)
280             datetime_today = datetime.datetime.strptime(today, tools.DEFAULT_SERVER_DATE_FORMAT)
281             limit_date = str((datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
282             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))
283             res_ids = [x[0] for x in cr.fetchall()]
284             res.append(('id', search_operator, res_ids))
285         return res
286
287     def _get_contract_reminder_fnc(self, cr, uid, ids, field_names, unknow_none, context=None):
288         res= {}
289         for record in self.browse(cr, uid, ids, context=context):
290             overdue = False
291             due_soon = False
292             total = 0
293             name = ''
294             for element in record.log_contracts:
295                 if element.state in ('open', 'toclose') and element.expiration_date:
296                     current_date_str = fields.date.context_today(self, cr, uid, context=context)
297                     due_time_str = element.expiration_date
298                     current_date = str_to_datetime(current_date_str)
299                     due_time = str_to_datetime(due_time_str)
300                     diff_time = (due_time-current_date).days
301                     if diff_time < 0:
302                         overdue = True
303                         total += 1
304                     if diff_time < 15 and diff_time >= 0:
305                             due_soon = True;
306                             total += 1
307                     if overdue or due_soon:
308                         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')
309                         if len(ids) > 0:
310                             #we display only the name of the oldest overdue/due soon contract
311                             name=(self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, ids[0], context=context).cost_subtype_id.name)
312
313             res[record.id] = {
314                 'contract_renewal_overdue': overdue,
315                 'contract_renewal_due_soon': due_soon,
316                 'contract_renewal_total': (total - 1), #we remove 1 from the real total for display purposes
317                 'contract_renewal_name': name,
318             }
319         return res
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_id': 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_id': 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='Last Odometer', 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='Has 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='Has 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_id': _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_id' in vals and vehicle.driver_id.id != vals['driver_id']:
412                 value = self.pool.get('res.partner').browse(cr,uid,vals['driver_id'],context=context).name
413                 olddriver = (vehicle.driver_id.name) or _('None')
414                 changes.append(_("Driver: from '%s' to '%s'") %(olddriver, value))
415             if 'state_id' in vals and vehicle.state_id.id != vals['state_id']:
416                 value = self.pool.get('fleet.vehicle.state').browse(cr,uid,vals['state_id'],context=context).name
417                 oldstate = vehicle.state_id.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         vehicle = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context)
472         odometer_unit = vehicle.odometer_unit
473         driver = vehicle.driver_id.id
474         return {
475             'value': {
476                 'odometer_unit': odometer_unit,
477                 'purchaser_id': driver,
478             }
479         }
480
481     def on_change_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
482         #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
483         #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
484         #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
485         #of 3.0/2=1.5)
486         #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
487         #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the 
488         #computation to 2 decimal
489         liter = float(liter)
490         price_per_liter = float(price_per_liter)
491         amount = float(amount)
492         if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
493             return {'value' : {'amount' : round(liter * price_per_liter,2),}}
494         elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
495             return {'value' : {'price_per_liter' : round(amount / liter,2),}}
496         elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
497             return {'value' : {'liter' : round(amount / price_per_liter,2),}}
498         else :
499             return {}
500
501     def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
502         #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
503         #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
504         #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
505         #of 3.0/2=1.5)
506         #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
507         #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the 
508         #computation to 2 decimal
509         liter = float(liter)
510         price_per_liter = float(price_per_liter)
511         amount = float(amount)
512         if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
513             return {'value' : {'amount' : round(liter * price_per_liter,2),}}
514         elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
515             return {'value' : {'liter' : round(amount / price_per_liter,2),}}
516         elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
517             return {'value' : {'price_per_liter' : round(amount / liter,2),}}
518         else :
519             return {}
520
521     def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
522         #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
523         #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
524         #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
525         #of 3.0/2=1.5)
526         #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
527         #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the 
528         #computation to 2 decimal
529         liter = float(liter)
530         price_per_liter = float(price_per_liter)
531         amount = float(amount)
532         if amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
533             return {'value': {'price_per_liter': round(amount / liter,2),}}
534         elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
535             return {'value': {'liter': round(amount / price_per_liter,2),}}
536         elif liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
537             return {'value': {'amount': round(liter * price_per_liter,2),}}
538         else :
539             return {}
540
541     def _get_default_service_type(self, cr, uid, context):
542         try:
543             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
544         except ValueError:
545             model_id = False
546         return model_id
547
548     _name = 'fleet.vehicle.log.fuel'
549     _description = 'Fuel log for vehicles'
550     _inherits = {'fleet.vehicle.cost': 'cost_id'}
551
552     _columns = {
553         'liter': fields.float('Liter'),
554         'price_per_liter': fields.float('Price Per Liter'),
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         'notes': fields.text('Notes'),
559         'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost'),
560         '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
561     }
562     _defaults = {
563         'date': fields.date.context_today,
564         'cost_subtype_id': _get_default_service_type,
565         'cost_type': 'fuel',
566     }
567
568
569 class fleet_vehicle_log_services(osv.Model):
570
571     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
572         if not vehicle_id:
573             return {}
574         vehicle = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context)
575         odometer_unit = vehicle.odometer_unit
576         driver = vehicle.driver_id.id
577         return {
578             'value': {
579                 'odometer_unit': odometer_unit,
580                 'purchaser_id': driver,
581             }
582         }
583
584     def _get_default_service_type(self, cr, uid, context):
585         try:
586             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
587         except ValueError:
588             model_id = False
589         return model_id
590
591     _inherits = {'fleet.vehicle.cost': 'cost_id'}
592     _name = 'fleet.vehicle.log.services'
593     _description = 'Services for vehicles'
594     _columns = {
595         'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
596         'inv_ref': fields.char('Invoice Reference', size=64),
597         'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
598         '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
599         'notes': fields.text('Notes'),
600         'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost'),
601     }
602     _defaults = {
603         'date': fields.date.context_today,
604         'cost_subtype_id': _get_default_service_type,
605         'cost_type': 'services'
606     }
607
608
609 class fleet_service_type(osv.Model):
610     _name = 'fleet.service.type'
611     _description = 'Type of services available on a vehicle'
612     _columns = {
613         'name': fields.char('Name', required=True, translate=True),
614         'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
615     }
616
617
618 class fleet_vehicle_log_contract(osv.Model):
619
620     def scheduler_manage_auto_costs(self, cr, uid, context=None):
621         #This method is called by a cron task
622         #It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
623         #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
624         #If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
625         #The created costs are associated to a contract thanks to the many2one field contract_id
626         #If the contract has no start_date, no cost will be created, even if the contract has recurring costs
627         vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
628         d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
629         contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
630         deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
631         for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
632             if not contract.start_date or contract.cost_frequency == 'no':
633                 continue
634             found = False
635             last_cost_date = contract.start_date
636             if contract.generated_cost_ids:
637                 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)
638                 if last_autogenerated_cost_id:
639                     found = True
640                     last_cost_date = vehicle_cost_obj.browse(cr, uid, last_autogenerated_cost_id[0], context=context).date
641             startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
642             if found:
643                 startdate += deltas.get(contract.cost_frequency)
644             while (startdate <= d) & (startdate <= datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
645                 data = {
646                     'amount': contract.cost_generated,
647                     'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
648                     'vehicle_id': contract.vehicle_id.id,
649                     'cost_subtype_id': contract.cost_subtype_id.id,
650                     'contract_id': contract.id,
651                     'auto_generated': True
652                 }
653                 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
654                 startdate += deltas.get(contract.cost_frequency)
655         return True
656
657     def scheduler_manage_contract_expiration(self, cr, uid, context=None):
658         #This method is called by a cron task
659         #It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
660         datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
661         limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
662         ids = self.search(cr, uid, ['&', ('state', '=', 'open'), ('expiration_date', '<', limit_date)], offset=0, limit=None, order=None, context=context, count=False)
663         res = {}
664         for contract in self.browse(cr, uid, ids, context=context):
665             if contract.vehicle_id.id in res:
666                 res[contract.vehicle_id.id] += 1
667             else:
668                 res[contract.vehicle_id.id] = 1
669
670         for vehicle, value in res.items():
671             self.pool.get('fleet.vehicle').message_post(cr, uid, vehicle, body=_('%s contract(s) need(s) to be renewed and/or closed!') % (str(value)), context=context)
672         return self.write(cr, uid, ids, {'state': 'toclose'}, context=context)
673
674     def run_scheduler(self, cr, uid, context=None):
675         self.scheduler_manage_auto_costs(cr, uid, context=context)
676         self.scheduler_manage_contract_expiration(cr, uid, context=context)
677         return True
678
679     def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
680         res = {}
681         for record in self.browse(cr, uid, ids, context=context):
682             name = record.vehicle_id.name
683             if record.cost_subtype_id.name:
684                 name += ' / '+ record.cost_subtype_id.name
685             if record.date:
686                 name += ' / '+ record.date
687             res[record.id] = name
688         return res
689
690     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
691         if not vehicle_id:
692             return {}
693         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
694         return {
695             'value': {
696                 'odometer_unit': odometer_unit,
697             }
698         }
699
700     def compute_next_year_date(self, strdate):
701         oneyear = datetime.timedelta(days=365)
702         curdate = str_to_datetime(strdate)
703         return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
704
705     def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
706         if (strdate):
707             return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
708         return {}
709
710     def get_days_left(self, cr, uid, ids, prop, unknow_none, context=None):
711         """return a dict with as value for each contract an integer
712         if contract is in an open state and is overdue, return 0
713         if contract is in a closed state, return -1
714         otherwise return the number of days before the contract expires
715         """
716         res = {}
717         for record in self.browse(cr, uid, ids, context=context):
718             if (record.expiration_date and (record.state == 'open' or record.state == 'toclose')):
719                 today = str_to_datetime(time.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
720                 renew_date = str_to_datetime(record.expiration_date)
721                 diff_time = (renew_date-today).days
722                 res[record.id] = diff_time > 0 and diff_time or 0
723             else:
724                 res[record.id] = -1
725         return res
726
727     def act_renew_contract(self, cr, uid, ids, context=None):
728         assert len(ids) == 1, "This operation should only be done for 1 single contract at a time, as it it suppose to open a window as result"
729         for element in self.browse(cr, uid, ids, context=context):
730             #compute end date
731             startdate = str_to_datetime(element.start_date)
732             enddate = str_to_datetime(element.expiration_date)
733             diffdate = (enddate - startdate)
734             default = {
735                 'date': fields.date.context_today(self, cr, uid, context=context),
736                 'start_date': datetime.datetime.strftime(str_to_datetime(element.expiration_date) + datetime.timedelta(days=1), tools.DEFAULT_SERVER_DATE_FORMAT),
737                 'expiration_date': datetime.datetime.strftime(enddate + diffdate, tools.DEFAULT_SERVER_DATE_FORMAT),
738             }
739             newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, element.id, default, context=context)
740         mod, modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
741         return {
742             'name':_("Renew Contract"),
743             'view_mode': 'form',
744             'view_id': modid,
745             'view_type': 'tree,form',
746             'res_model': 'fleet.vehicle.log.contract',
747             'type': 'ir.actions.act_window',
748             'nodestroy': True,
749             'domain': '[]',
750             'res_id': newid,
751             'context': {'active_id':newid}, 
752         }
753
754     def _get_default_contract_type(self, cr, uid, context=None):
755         try:
756             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
757         except ValueError:
758             model_id = False
759         return model_id
760
761     def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
762         totalsum = 0.0
763         for element in cost_ids:
764             if element and len(element) == 3 and element[2] is not False:
765                 totalsum += element[2].get('amount', 0.0)
766         return {
767             'value': {
768                 'sum_cost': totalsum,
769             }
770         }
771
772     def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
773         res = {}
774         for contract in self.browse(cr, uid, ids, context=context):
775             totalsum = 0
776             for cost in contract.cost_ids:
777                 totalsum += cost.amount
778             res[contract.id] = totalsum
779         return res
780
781     _inherits = {'fleet.vehicle.cost': 'cost_id'}
782     _name = 'fleet.vehicle.log.contract'
783     _description = 'Contract information on a vehicle'
784     _order='state desc,expiration_date'
785     _columns = {
786         'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
787         'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
788         'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
789         'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
790         'insurer_id' :fields.many2one('res.partner', 'Supplier'),
791         'purchaser_id': fields.many2one('res.partner', 'Contractor', help='Person to which the contract is signed for'),
792         'ins_ref': fields.char('Contract Reference', size=64),
793         'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')], 'Status', readonly=True, help='Choose wheter the contract is still valid or not'),
794         'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract'),
795         '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"),
796         '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),
797         'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs', ondelete='cascade'),
798         'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
799         'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost'),
800         '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
801     }
802     _defaults = {
803         'purchaser_id': lambda self, cr, uid, ctx: self.pool.get('res.users').browse(cr, uid, uid, context=ctx).partner_id.id or False,
804         'date': fields.date.context_today,
805         'start_date': fields.date.context_today,
806         'state':'open',
807         'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
808         'cost_frequency': 'no',
809         'cost_subtype_id': _get_default_contract_type,
810         'cost_type': 'contract',
811     }
812
813     def copy(self, cr, uid, id, default=None, context=None):
814         if default is None:
815             default = {}
816         today = fields.date.context_today(self, cr, uid, context=context)
817         default['date'] = today
818         default['start_date'] = today
819         default['expiration_date'] = self.compute_next_year_date(today)
820         default['ins_ref'] = ''
821         default['state'] = 'open'
822         default['notes'] = ''
823         return super(fleet_vehicle_log_contract, self).copy(cr, uid, id, default, context=context)
824
825     def contract_close(self, cr, uid, ids, context=None):
826         return self.write(cr, uid, ids, {'state': 'closed'}, context=context)
827
828     def contract_open(self, cr, uid, ids, context=None):
829         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
830
831 class fleet_contract_state(osv.Model):
832     _name = 'fleet.contract.state'
833     _description = 'Contains the different possible status of a leasing contract'
834
835     _columns = {
836         'name':fields.char('Contract Status', size=64, required=True),
837     }