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