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