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