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