[MERGE] forward port of branch 8.0 up to c92e70b
[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 not name:
430                 name = record.date
431             elif record.date:
432                 name += ' / '+ record.date
433             res[record.id] = name
434         return res
435
436     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
437         if not vehicle_id:
438             return {}
439         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
440         return {
441             'value': {
442                 'unit': odometer_unit,
443             }
444         }
445
446     _columns = {
447         'name': fields.function(_vehicle_log_name_get_fnc, type="char", string='Name', store=True),
448         'date': fields.date('Date'),
449         'value': fields.float('Odometer Value', group_operator="max"),
450         'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True),
451         'unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
452     }
453     _defaults = {
454         'date': fields.date.context_today,
455     }
456
457
458 class fleet_vehicle_log_fuel(osv.Model):
459
460     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
461         if not vehicle_id:
462             return {}
463         vehicle = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context)
464         odometer_unit = vehicle.odometer_unit
465         driver = vehicle.driver_id.id
466         return {
467             'value': {
468                 'odometer_unit': odometer_unit,
469                 'purchaser_id': driver,
470             }
471         }
472
473     def on_change_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
474         #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
475         #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
476         #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
477         #of 3.0/2=1.5)
478         #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
479         #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the 
480         #computation to 2 decimal
481         liter = float(liter)
482         price_per_liter = float(price_per_liter)
483         amount = float(amount)
484         if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
485             return {'value' : {'amount' : round(liter * price_per_liter,2),}}
486         elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
487             return {'value' : {'price_per_liter' : round(amount / liter,2),}}
488         elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
489             return {'value' : {'liter' : round(amount / price_per_liter,2),}}
490         else :
491             return {}
492
493     def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
494         #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
495         #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
496         #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
497         #of 3.0/2=1.5)
498         #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
499         #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the 
500         #computation to 2 decimal
501         liter = float(liter)
502         price_per_liter = float(price_per_liter)
503         amount = float(amount)
504         if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
505             return {'value' : {'amount' : round(liter * price_per_liter,2),}}
506         elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
507             return {'value' : {'liter' : round(amount / price_per_liter,2),}}
508         elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
509             return {'value' : {'price_per_liter' : round(amount / liter,2),}}
510         else :
511             return {}
512
513     def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
514         #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
515         #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
516         #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
517         #of 3.0/2=1.5)
518         #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
519         #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the 
520         #computation to 2 decimal
521         liter = float(liter)
522         price_per_liter = float(price_per_liter)
523         amount = float(amount)
524         if amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
525             return {'value': {'price_per_liter': round(amount / liter,2),}}
526         elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
527             return {'value': {'liter': round(amount / price_per_liter,2),}}
528         elif liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
529             return {'value': {'amount': round(liter * price_per_liter,2),}}
530         else :
531             return {}
532
533     def _get_default_service_type(self, cr, uid, context):
534         try:
535             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
536         except ValueError:
537             model_id = False
538         return model_id
539
540     _name = 'fleet.vehicle.log.fuel'
541     _description = 'Fuel log for vehicles'
542     _inherits = {'fleet.vehicle.cost': 'cost_id'}
543
544     _columns = {
545         'liter': fields.float('Liter'),
546         'price_per_liter': fields.float('Price Per Liter'),
547         'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
548         'inv_ref': fields.char('Invoice Reference', size=64),
549         'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
550         'notes': fields.text('Notes'),
551         'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade'),
552         '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
553     }
554     _defaults = {
555         'date': fields.date.context_today,
556         'cost_subtype_id': _get_default_service_type,
557         'cost_type': 'fuel',
558     }
559
560
561 class fleet_vehicle_log_services(osv.Model):
562
563     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
564         if not vehicle_id:
565             return {}
566         vehicle = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context)
567         odometer_unit = vehicle.odometer_unit
568         driver = vehicle.driver_id.id
569         return {
570             'value': {
571                 'odometer_unit': odometer_unit,
572                 'purchaser_id': driver,
573             }
574         }
575
576     def _get_default_service_type(self, cr, uid, context):
577         try:
578             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
579         except ValueError:
580             model_id = False
581         return model_id
582
583     _inherits = {'fleet.vehicle.cost': 'cost_id'}
584     _name = 'fleet.vehicle.log.services'
585     _description = 'Services for vehicles'
586     _columns = {
587         'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
588         'inv_ref': fields.char('Invoice Reference'),
589         'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
590         '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
591         'notes': fields.text('Notes'),
592         'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade'),
593     }
594     _defaults = {
595         'date': fields.date.context_today,
596         'cost_subtype_id': _get_default_service_type,
597         'cost_type': 'services'
598     }
599
600
601 class fleet_service_type(osv.Model):
602     _name = 'fleet.service.type'
603     _description = 'Type of services available on a vehicle'
604     _columns = {
605         'name': fields.char('Name', required=True, translate=True),
606         'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
607     }
608
609
610 class fleet_vehicle_log_contract(osv.Model):
611
612     def scheduler_manage_auto_costs(self, cr, uid, context=None):
613         #This method is called by a cron task
614         #It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
615         #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
616         #If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
617         #The created costs are associated to a contract thanks to the many2one field contract_id
618         #If the contract has no start_date, no cost will be created, even if the contract has recurring costs
619         vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
620         d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
621         contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
622         deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
623         for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
624             if not contract.start_date or contract.cost_frequency == 'no':
625                 continue
626             found = False
627             last_cost_date = contract.start_date
628             if contract.generated_cost_ids:
629                 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)
630                 if last_autogenerated_cost_id:
631                     found = True
632                     last_cost_date = vehicle_cost_obj.browse(cr, uid, last_autogenerated_cost_id[0], context=context).date
633             startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
634             if found:
635                 startdate += deltas.get(contract.cost_frequency)
636             while (startdate <= d) & (startdate <= datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
637                 data = {
638                     'amount': contract.cost_generated,
639                     'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
640                     'vehicle_id': contract.vehicle_id.id,
641                     'cost_subtype_id': contract.cost_subtype_id.id,
642                     'contract_id': contract.id,
643                     'auto_generated': True
644                 }
645                 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
646                 startdate += deltas.get(contract.cost_frequency)
647         return True
648
649     def scheduler_manage_contract_expiration(self, cr, uid, context=None):
650         #This method is called by a cron task
651         #It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
652         datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
653         limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
654         ids = self.search(cr, uid, ['&', ('state', '=', 'open'), ('expiration_date', '<', limit_date)], offset=0, limit=None, order=None, context=context, count=False)
655         res = {}
656         for contract in self.browse(cr, uid, ids, context=context):
657             if contract.vehicle_id.id in res:
658                 res[contract.vehicle_id.id] += 1
659             else:
660                 res[contract.vehicle_id.id] = 1
661
662         for vehicle, value in res.items():
663             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)
664         return self.write(cr, uid, ids, {'state': 'toclose'}, context=context)
665
666     def run_scheduler(self, cr, uid, context=None):
667         self.scheduler_manage_auto_costs(cr, uid, context=context)
668         self.scheduler_manage_contract_expiration(cr, uid, context=context)
669         return True
670
671     def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
672         res = {}
673         for record in self.browse(cr, uid, ids, context=context):
674             name = record.vehicle_id.name
675             if record.cost_subtype_id.name:
676                 name += ' / '+ record.cost_subtype_id.name
677             if record.date:
678                 name += ' / '+ record.date
679             res[record.id] = name
680         return res
681
682     def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
683         if not vehicle_id:
684             return {}
685         odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
686         return {
687             'value': {
688                 'odometer_unit': odometer_unit,
689             }
690         }
691
692     def compute_next_year_date(self, strdate):
693         oneyear = datetime.timedelta(days=365)
694         curdate = str_to_datetime(strdate)
695         return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
696
697     def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
698         if (strdate):
699             return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
700         return {}
701
702     def get_days_left(self, cr, uid, ids, prop, unknow_none, context=None):
703         """return a dict with as value for each contract an integer
704         if contract is in an open state and is overdue, return 0
705         if contract is in a closed state, return -1
706         otherwise return the number of days before the contract expires
707         """
708         res = {}
709         for record in self.browse(cr, uid, ids, context=context):
710             if (record.expiration_date and (record.state == 'open' or record.state == 'toclose')):
711                 today = str_to_datetime(time.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
712                 renew_date = str_to_datetime(record.expiration_date)
713                 diff_time = (renew_date-today).days
714                 res[record.id] = diff_time > 0 and diff_time or 0
715             else:
716                 res[record.id] = -1
717         return res
718
719     def act_renew_contract(self, cr, uid, ids, context=None):
720         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"
721         for element in self.browse(cr, uid, ids, context=context):
722             #compute end date
723             startdate = str_to_datetime(element.start_date)
724             enddate = str_to_datetime(element.expiration_date)
725             diffdate = (enddate - startdate)
726             default = {
727                 'date': fields.date.context_today(self, cr, uid, context=context),
728                 'start_date': datetime.datetime.strftime(str_to_datetime(element.expiration_date) + datetime.timedelta(days=1), tools.DEFAULT_SERVER_DATE_FORMAT),
729                 'expiration_date': datetime.datetime.strftime(enddate + diffdate, tools.DEFAULT_SERVER_DATE_FORMAT),
730             }
731             newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, element.id, default, context=context)
732         mod, modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
733         return {
734             'name':_("Renew Contract"),
735             'view_mode': 'form',
736             'view_id': modid,
737             'view_type': 'tree,form',
738             'res_model': 'fleet.vehicle.log.contract',
739             'type': 'ir.actions.act_window',
740             'nodestroy': True,
741             'domain': '[]',
742             'res_id': newid,
743             'context': {'active_id':newid}, 
744         }
745
746     def _get_default_contract_type(self, cr, uid, context=None):
747         try:
748             model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
749         except ValueError:
750             model_id = False
751         return model_id
752
753     def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
754         totalsum = 0.0
755         for element in cost_ids:
756             if element and len(element) == 3 and isinstance(element[2], dict):
757                 totalsum += element[2].get('amount', 0.0)
758         return {
759             'value': {
760                 'sum_cost': totalsum,
761             }
762         }
763
764     def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
765         res = {}
766         for contract in self.browse(cr, uid, ids, context=context):
767             totalsum = 0
768             for cost in contract.cost_ids:
769                 totalsum += cost.amount
770             res[contract.id] = totalsum
771         return res
772
773     _inherits = {'fleet.vehicle.cost': 'cost_id'}
774     _name = 'fleet.vehicle.log.contract'
775     _description = 'Contract information on a vehicle'
776     _order='state desc,expiration_date'
777     _columns = {
778         'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
779         'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
780         'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
781         'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
782         'insurer_id' :fields.many2one('res.partner', 'Supplier'),
783         'purchaser_id': fields.many2one('res.partner', 'Contractor', help='Person to which the contract is signed for'),
784         'ins_ref': fields.char('Contract Reference', size=64, copy=False),
785         'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')],
786                                   'Status', readonly=True, help='Choose wheter the contract is still valid or not',
787                                   copy=False),
788         'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract', copy=False),
789         '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"),
790         '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),
791         'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs'),
792         'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
793         'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade'),
794         '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
795     }
796     _defaults = {
797         'purchaser_id': lambda self, cr, uid, ctx: self.pool.get('res.users').browse(cr, uid, uid, context=ctx).partner_id.id or False,
798         'date': fields.date.context_today,
799         'start_date': fields.date.context_today,
800         'state':'open',
801         'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
802         'cost_frequency': 'no',
803         'cost_subtype_id': _get_default_contract_type,
804         'cost_type': 'contract',
805     }
806
807     def contract_close(self, cr, uid, ids, context=None):
808         return self.write(cr, uid, ids, {'state': 'closed'}, context=context)
809
810     def contract_open(self, cr, uid, ids, context=None):
811         return self.write(cr, uid, ids, {'state': 'open'}, context=context)
812
813 class fleet_contract_state(osv.Model):
814     _name = 'fleet.contract.state'
815     _description = 'Contains the different possible status of a leasing contract'
816
817     _columns = {
818         'name':fields.char('Contract Status', required=True),
819     }