1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from openerp.osv import fields, osv
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
30 def str_to_datetime(strdate):
31 return datetime.datetime.strptime(strdate, tools.DEFAULT_SERVER_DATE_FORMAT)
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'
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
45 def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
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
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)
57 'name': fields.related('vehicle_id', 'name', type="char", string='Name', store=True),
58 'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True, help='Vehicle concerned by this log'),
59 'cost_subtype_id': fields.many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost'),
60 'amount': fields.float('Total Price'),
61 'cost_type': fields.selection([('contract', 'Contract'), ('services','Services'), ('fuel','Fuel'), ('other','Other')], 'Category of the cost', help='For internal purpose only', required=True),
62 'parent_id': fields.many2one('fleet.vehicle.cost', 'Parent', help='Parent cost to this current cost'),
63 'cost_ids': fields.one2many('fleet.vehicle.cost', 'parent_id', 'Included Services'),
64 'odometer_id': fields.many2one('fleet.vehicle.odometer', 'Odometer', help='Odometer measure of the vehicle at the moment of this log'),
65 'odometer': fields.function(_get_odometer, fnct_inv=_set_odometer, type='float', string='Odometer Value', help='Odometer measure of the vehicle at the moment of this log'),
66 'odometer_unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
67 'date' :fields.date('Date',help='Date when the cost has been executed'),
68 'contract_id': fields.many2one('fleet.vehicle.log.contract', 'Contract', help='Contract attached to this cost'),
69 'auto_generated': fields.boolean('Automatically Generated', readonly=True, required=True),
76 def create(self, cr, uid, data, context=None):
77 #make sure that the data are consistent with values of parent and contract records given
78 if 'parent_id' in data and data['parent_id']:
79 parent = self.browse(cr, uid, data['parent_id'], context=context)
80 data['vehicle_id'] = parent.vehicle_id.id
81 data['date'] = parent.date
82 data['cost_type'] = parent.cost_type
83 if 'contract_id' in data and data['contract_id']:
84 contract = self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, data['contract_id'], context=context)
85 data['vehicle_id'] = contract.vehicle_id.id
86 data['cost_subtype_id'] = contract.cost_subtype_id.id
87 data['cost_type'] = contract.cost_type
88 if 'odometer' in data and not data['odometer']:
89 #if received value for odometer is 0, then remove it from the data as it would result to the creation of a
90 #odometer log with 0, which is to be avoided
92 return super(fleet_vehicle_cost, self).create(cr, uid, data, context=context)
95 class fleet_vehicle_tag(osv.Model):
96 _name = 'fleet.vehicle.tag'
98 'name': fields.char('Name', required=True, translate=True),
101 class fleet_vehicle_state(osv.Model):
102 _name = 'fleet.vehicle.state'
103 _order = 'sequence asc'
105 'name': fields.char('Name', required=True),
106 'sequence': fields.integer('Sequence', help="Used to order the note stages")
108 _sql_constraints = [('fleet_state_name_unique','unique(name)', 'State name already exists')]
111 class fleet_vehicle_model(osv.Model):
113 def _model_name_get_fnc(self, cr, uid, ids, field_name, arg, context=None):
115 for record in self.browse(cr, uid, ids, context=context):
116 name = record.modelname
117 if record.brand_id.name:
118 name = record.brand_id.name + ' / ' + name
119 res[record.id] = name
122 def on_change_brand(self, cr, uid, ids, model_id, context=None):
124 return {'value': {'image_medium': False}}
125 brand = self.pool.get('fleet.vehicle.model.brand').browse(cr, uid, model_id, context=context)
128 'image_medium': brand.image,
132 _name = 'fleet.vehicle.model'
133 _description = 'Model of a vehicle'
137 'name': fields.function(_model_name_get_fnc, type="char", string='Name', store=True),
138 'modelname': fields.char('Model name', required=True),
139 'brand_id': fields.many2one('fleet.vehicle.model.brand', 'Model Brand', required=True, help='Brand of the vehicle'),
140 'vendors': fields.many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors'),
141 'image': fields.related('brand_id', 'image', type="binary", string="Logo"),
142 'image_medium': fields.related('brand_id', 'image_medium', type="binary", string="Logo (medium)"),
143 'image_small': fields.related('brand_id', 'image_small', type="binary", string="Logo (small)"),
147 class fleet_vehicle_model_brand(osv.Model):
148 _name = 'fleet.vehicle.model.brand'
149 _description = 'Brand model of the vehicle'
153 def _get_image(self, cr, uid, ids, name, args, context=None):
154 result = dict.fromkeys(ids, False)
155 for obj in self.browse(cr, uid, ids, context=context):
156 result[obj.id] = tools.image_get_resized_images(obj.image)
159 def _set_image(self, cr, uid, id, name, value, args, context=None):
160 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
163 'name': fields.char('Brand Name', required=True),
164 'image': fields.binary("Logo",
165 help="This field holds the image used as logo for the brand, limited to 1024x1024px."),
166 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
167 string="Medium-sized photo", type="binary", multi="_get_image",
169 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
171 help="Medium-sized logo of the brand. It is automatically "\
172 "resized as a 128x128px image, with aspect ratio preserved. "\
173 "Use this field in form views or some kanban views."),
174 'image_small': fields.function(_get_image, fnct_inv=_set_image,
175 string="Smal-sized photo", type="binary", multi="_get_image",
177 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
179 help="Small-sized photo of the brand. It is automatically "\
180 "resized as a 64x64px image, with aspect ratio preserved. "\
181 "Use this field anywhere a small image is required."),
185 class fleet_vehicle(osv.Model):
187 _inherit = 'mail.thread'
189 def _vehicle_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
191 for record in self.browse(cr, uid, ids, context=context):
192 res[record.id] = record.model_id.brand_id.name + '/' + record.model_id.modelname + ' / ' + record.license_plate
195 def return_action_to_open(self, cr, uid, ids, context=None):
196 """ This opens the xml view specified in xml_id for the current vehicle """
199 if context.get('xml_id'):
200 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet', context['xml_id'], context=context)
201 res['context'] = context
202 res['context'].update({'default_vehicle_id': ids[0]})
203 res['domain'] = [('vehicle_id','=', ids[0])]
207 def act_show_log_cost(self, cr, uid, ids, context=None):
208 """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
209 @return: the costs log view
213 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet','fleet_vehicle_costs_act', context=context)
214 res['context'] = context
215 res['context'].update({
216 'default_vehicle_id': ids[0],
217 'search_default_parent_false': True
219 res['domain'] = [('vehicle_id','=', ids[0])]
222 def _get_odometer(self, cr, uid, ids, odometer_id, arg, context):
223 res = dict.fromkeys(ids, 0)
224 for record in self.browse(cr,uid,ids,context=context):
225 ids = self.pool.get('fleet.vehicle.odometer').search(cr, uid, [('vehicle_id', '=', record.id)], limit=1, order='value desc')
227 res[record.id] = self.pool.get('fleet.vehicle.odometer').browse(cr, uid, ids[0], context=context).value
230 def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
232 date = fields.date.context_today(self, cr, uid, context=context)
233 data = {'value': value, 'date': date, 'vehicle_id': id}
234 return self.pool.get('fleet.vehicle.odometer').create(cr, uid, data, context=context)
236 def _search_get_overdue_contract_reminder(self, cr, uid, obj, name, args, context):
238 for field, operator, value in args:
239 assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
240 if (operator == '=' and value == True) or (operator in ('<>', '!=') and value == False):
241 search_operator = 'in'
243 search_operator = 'not in'
244 today = fields.date.context_today(self, cr, uid, context=context)
245 cr.execute('select cost.vehicle_id, count(contract.id) as contract_number FROM fleet_vehicle_cost cost left join fleet_vehicle_log_contract contract on contract.cost_id = cost.id WHERE contract.expiration_date is not null AND contract.expiration_date < %s AND contract.state IN (\'open\', \'toclose\') GROUP BY cost.vehicle_id', (today,))
246 res_ids = [x[0] for x in cr.fetchall()]
247 res.append(('id', search_operator, res_ids))
250 def _search_contract_renewal_due_soon(self, cr, uid, obj, name, args, context):
252 for field, operator, value in args:
253 assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
254 if (operator == '=' and value == True) or (operator in ('<>', '!=') and value == False):
255 search_operator = 'in'
257 search_operator = 'not in'
258 today = fields.date.context_today(self, cr, uid, context=context)
259 datetime_today = datetime.datetime.strptime(today, tools.DEFAULT_SERVER_DATE_FORMAT)
260 limit_date = str((datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
261 cr.execute('select cost.vehicle_id, count(contract.id) as contract_number FROM fleet_vehicle_cost cost left join fleet_vehicle_log_contract contract on contract.cost_id = cost.id WHERE contract.expiration_date is not null AND contract.expiration_date > %s AND contract.expiration_date < %s AND contract.state IN (\'open\', \'toclose\') GROUP BY cost.vehicle_id', (today, limit_date))
262 res_ids = [x[0] for x in cr.fetchall()]
263 res.append(('id', search_operator, res_ids))
266 def _get_contract_reminder_fnc(self, cr, uid, ids, field_names, unknow_none, context=None):
268 for record in self.browse(cr, uid, ids, context=context):
273 for element in record.log_contracts:
274 if element.state in ('open', 'toclose') and element.expiration_date:
275 current_date_str = fields.date.context_today(self, cr, uid, context=context)
276 due_time_str = element.expiration_date
277 current_date = str_to_datetime(current_date_str)
278 due_time = str_to_datetime(due_time_str)
279 diff_time = (due_time-current_date).days
283 if diff_time < 15 and diff_time >= 0:
286 if overdue or due_soon:
287 ids = self.pool.get('fleet.vehicle.log.contract').search(cr,uid,[('vehicle_id', '=', record.id), ('state', 'in', ('open', 'toclose'))], limit=1, order='expiration_date asc')
289 #we display only the name of the oldest overdue/due soon contract
290 name=(self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, ids[0], context=context).cost_subtype_id.name)
293 'contract_renewal_overdue': overdue,
294 'contract_renewal_due_soon': due_soon,
295 'contract_renewal_total': (total - 1), #we remove 1 from the real total for display purposes
296 'contract_renewal_name': name,
300 def _get_default_state(self, cr, uid, context):
302 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'vehicle_state_active')
307 def _count_all(self, cr, uid, ids, field_name, arg, context=None):
308 Odometer = self.pool['fleet.vehicle.odometer']
309 LogFuel = self.pool['fleet.vehicle.log.fuel']
310 LogService = self.pool['fleet.vehicle.log.services']
311 LogContract = self.pool['fleet.vehicle.log.contract']
312 Cost = self.pool['fleet.vehicle.cost']
315 'odometer_count': Odometer.search_count(cr, uid, [('vehicle_id', '=', vehicle_id)], context=context),
316 'fuel_logs_count': LogFuel.search_count(cr, uid, [('vehicle_id', '=', vehicle_id)], context=context),
317 'service_count': LogService.search_count(cr, uid, [('vehicle_id', '=', vehicle_id)], context=context),
318 'contract_count': LogContract.search_count(cr, uid, [('vehicle_id', '=', vehicle_id)], context=context),
319 'cost_count': Cost.search_count(cr, uid, [('vehicle_id', '=', vehicle_id), ('parent_id', '=', False)], context=context)
321 for vehicle_id in ids
324 _name = 'fleet.vehicle'
325 _description = 'Information on a vehicle'
326 _order= 'license_plate asc'
328 'name': fields.function(_vehicle_name_get_fnc, type="char", string='Name', store=True),
329 'company_id': fields.many2one('res.company', 'Company'),
330 'license_plate': fields.char('License Plate', required=True, help='License plate number of the vehicle (ie: plate number for a car)'),
331 'vin_sn': fields.char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False),
332 'driver_id': fields.many2one('res.partner', 'Driver', help='Driver of the vehicle'),
333 'model_id': fields.many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle'),
334 'log_fuel': fields.one2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs'),
335 'log_services': fields.one2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs'),
336 'log_contracts': fields.one2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts'),
337 'cost_count': fields.function(_count_all, type='integer', string="Costs" , multi=True),
338 'contract_count': fields.function(_count_all, type='integer', string='Contracts', multi=True),
339 'service_count': fields.function(_count_all, type='integer', string='Services', multi=True),
340 'fuel_logs_count': fields.function(_count_all, type='integer', string='Fuel Logs', multi=True),
341 'odometer_count': fields.function(_count_all, type='integer', string='Odometer', multi=True),
342 'acquisition_date': fields.date('Acquisition Date', required=False, help='Date when the vehicle has been bought'),
343 'color': fields.char('Color', help='Color of the vehicle'),
344 'state_id': fields.many2one('fleet.vehicle.state', 'State', help='Current state of the vehicle', ondelete="set null"),
345 'location': fields.char('Location', help='Location of the vehicle (garage, ...)'),
346 'seats': fields.integer('Seats Number', help='Number of seats of the vehicle'),
347 'doors': fields.integer('Doors Number', help='Number of doors of the vehicle'),
348 'tag_ids' :fields.many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id','tag_id', 'Tags', copy=False),
349 'odometer': fields.function(_get_odometer, fnct_inv=_set_odometer, type='float', string='Last Odometer', help='Odometer measure of the vehicle at the moment of this log'),
350 'odometer_unit': fields.selection([('kilometers', 'Kilometers'),('miles','Miles')], 'Odometer Unit', help='Unit of the odometer ',required=True),
351 'transmission': fields.selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle'),
352 'fuel_type': fields.selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle'),
353 'horsepower': fields.integer('Horsepower'),
354 'horsepower_tax': fields.float('Horsepower Taxation'),
355 'power': fields.integer('Power', help='Power in kW of the vehicle'),
356 'co2': fields.float('CO2 Emissions', help='CO2 emissions of the vehicle'),
357 'image': fields.related('model_id', 'image', type="binary", string="Logo"),
358 'image_medium': fields.related('model_id', 'image_medium', type="binary", string="Logo (medium)"),
359 'image_small': fields.related('model_id', 'image_small', type="binary", string="Logo (small)"),
360 'contract_renewal_due_soon': fields.function(_get_contract_reminder_fnc, fnct_search=_search_contract_renewal_due_soon, type="boolean", string='Has Contracts to renew', multi='contract_info'),
361 'contract_renewal_overdue': fields.function(_get_contract_reminder_fnc, fnct_search=_search_get_overdue_contract_reminder, type="boolean", string='Has Contracts Overdued', multi='contract_info'),
362 'contract_renewal_name': fields.function(_get_contract_reminder_fnc, type="text", string='Name of contract to renew soon', multi='contract_info'),
363 'contract_renewal_total': fields.function(_get_contract_reminder_fnc, type="integer", string='Total of contracts due or overdue minus one', multi='contract_info'),
364 'car_value': fields.float('Car Value', help='Value of the bought vehicle'),
369 'odometer_unit': 'kilometers',
370 'state_id': _get_default_state,
373 def on_change_model(self, cr, uid, ids, model_id, context=None):
376 model = self.pool.get('fleet.vehicle.model').browse(cr, uid, model_id, context=context)
379 'image_medium': model.image,
383 def create(self, cr, uid, data, context=None):
384 context = dict(context or {}, mail_create_nolog=True)
385 vehicle_id = super(fleet_vehicle, self).create(cr, uid, data, context=context)
386 vehicle = self.browse(cr, uid, vehicle_id, context=context)
387 self.message_post(cr, uid, [vehicle_id], body=_('%s %s has been added to the fleet!') % (vehicle.model_id.name,vehicle.license_plate), context=context)
390 def write(self, cr, uid, ids, vals, context=None):
392 This function write an entry in the openchatter whenever we change important information
393 on the vehicle like the model, the drive, the state of the vehicle or its license plate
395 for vehicle in self.browse(cr, uid, ids, context):
397 if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
398 value = self.pool.get('fleet.vehicle.model').browse(cr,uid,vals['model_id'],context=context).name
399 oldmodel = vehicle.model_id.name or _('None')
400 changes.append(_("Model: from '%s' to '%s'") %(oldmodel, value))
401 if 'driver_id' in vals and vehicle.driver_id.id != vals['driver_id']:
402 value = self.pool.get('res.partner').browse(cr,uid,vals['driver_id'],context=context).name
403 olddriver = (vehicle.driver_id.name) or _('None')
404 changes.append(_("Driver: from '%s' to '%s'") %(olddriver, value))
405 if 'state_id' in vals and vehicle.state_id.id != vals['state_id']:
406 value = self.pool.get('fleet.vehicle.state').browse(cr,uid,vals['state_id'],context=context).name
407 oldstate = vehicle.state_id.name or _('None')
408 changes.append(_("State: from '%s' to '%s'") %(oldstate, value))
409 if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
410 old_license_plate = vehicle.license_plate or _('None')
411 changes.append(_("License Plate: from '%s' to '%s'") %(old_license_plate, vals['license_plate']))
414 self.message_post(cr, uid, [vehicle.id], body=", ".join(changes), context=context)
416 vehicle_id = super(fleet_vehicle,self).write(cr, uid, ids, vals, context)
420 class fleet_vehicle_odometer(osv.Model):
421 _name='fleet.vehicle.odometer'
422 _description='Odometer log for a vehicle'
425 def _vehicle_log_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
427 for record in self.browse(cr, uid, ids, context=context):
428 name = record.vehicle_id.name
430 name = name+ ' / '+ str(record.date)
431 res[record.id] = name
434 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
437 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
440 'unit': odometer_unit,
445 'name': fields.function(_vehicle_log_name_get_fnc, type="char", string='Name', store=True),
446 'date': fields.date('Date'),
447 'value': fields.float('Odometer Value', group_operator="max"),
448 'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True),
449 'unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
452 'date': fields.date.context_today,
456 class fleet_vehicle_log_fuel(osv.Model):
458 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
461 vehicle = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context)
462 odometer_unit = vehicle.odometer_unit
463 driver = vehicle.driver_id.id
466 'odometer_unit': odometer_unit,
467 'purchaser_id': driver,
471 def on_change_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
472 #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
473 #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
474 #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
476 #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
477 #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
478 #computation to 2 decimal
480 price_per_liter = float(price_per_liter)
481 amount = float(amount)
482 if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
483 return {'value' : {'amount' : round(liter * price_per_liter,2),}}
484 elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
485 return {'value' : {'price_per_liter' : round(amount / liter,2),}}
486 elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
487 return {'value' : {'liter' : round(amount / price_per_liter,2),}}
491 def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
492 #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
493 #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
494 #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
496 #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
497 #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
498 #computation to 2 decimal
500 price_per_liter = float(price_per_liter)
501 amount = float(amount)
502 if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
503 return {'value' : {'amount' : round(liter * price_per_liter,2),}}
504 elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
505 return {'value' : {'liter' : round(amount / price_per_liter,2),}}
506 elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
507 return {'value' : {'price_per_liter' : round(amount / liter,2),}}
511 def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
512 #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
513 #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
514 #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
516 #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
517 #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
518 #computation to 2 decimal
520 price_per_liter = float(price_per_liter)
521 amount = float(amount)
522 if amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
523 return {'value': {'price_per_liter': round(amount / liter,2),}}
524 elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
525 return {'value': {'liter': round(amount / price_per_liter,2),}}
526 elif liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
527 return {'value': {'amount': round(liter * price_per_liter,2),}}
531 def _get_default_service_type(self, cr, uid, context):
533 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
538 _name = 'fleet.vehicle.log.fuel'
539 _description = 'Fuel log for vehicles'
540 _inherits = {'fleet.vehicle.cost': 'cost_id'}
543 'liter': fields.float('Liter'),
544 'price_per_liter': fields.float('Price Per Liter'),
545 'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
546 'inv_ref': fields.char('Invoice Reference', size=64),
547 'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
548 'notes': fields.text('Notes'),
549 'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade'),
550 'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
553 'date': fields.date.context_today,
554 'cost_subtype_id': _get_default_service_type,
559 class fleet_vehicle_log_services(osv.Model):
561 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
564 vehicle = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context)
565 odometer_unit = vehicle.odometer_unit
566 driver = vehicle.driver_id.id
569 'odometer_unit': odometer_unit,
570 'purchaser_id': driver,
574 def _get_default_service_type(self, cr, uid, context):
576 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
581 _inherits = {'fleet.vehicle.cost': 'cost_id'}
582 _name = 'fleet.vehicle.log.services'
583 _description = 'Services for vehicles'
585 'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
586 'inv_ref': fields.char('Invoice Reference'),
587 'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
588 'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
589 'notes': fields.text('Notes'),
590 'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade'),
593 'date': fields.date.context_today,
594 'cost_subtype_id': _get_default_service_type,
595 'cost_type': 'services'
599 class fleet_service_type(osv.Model):
600 _name = 'fleet.service.type'
601 _description = 'Type of services available on a vehicle'
603 'name': fields.char('Name', required=True, translate=True),
604 'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
608 class fleet_vehicle_log_contract(osv.Model):
610 def scheduler_manage_auto_costs(self, cr, uid, context=None):
611 #This method is called by a cron task
612 #It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
613 #For example, if a contract has a reccuring cost of 200 with a weekly frequency, this method creates a cost of 200 on the first day of each week, from the date of the last recurring costs in the database to today
614 #If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
615 #The created costs are associated to a contract thanks to the many2one field contract_id
616 #If the contract has no start_date, no cost will be created, even if the contract has recurring costs
617 vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
618 d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
619 contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
620 deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
621 for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
622 if not contract.start_date or contract.cost_frequency == 'no':
625 last_cost_date = contract.start_date
626 if contract.generated_cost_ids:
627 last_autogenerated_cost_id = vehicle_cost_obj.search(cr, uid, ['&', ('contract_id','=',contract.id), ('auto_generated','=',True)], offset=0, limit=1, order='date desc',context=context, count=False)
628 if last_autogenerated_cost_id:
630 last_cost_date = vehicle_cost_obj.browse(cr, uid, last_autogenerated_cost_id[0], context=context).date
631 startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
633 startdate += deltas.get(contract.cost_frequency)
634 while (startdate <= d) & (startdate <= datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
636 'amount': contract.cost_generated,
637 'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
638 'vehicle_id': contract.vehicle_id.id,
639 'cost_subtype_id': contract.cost_subtype_id.id,
640 'contract_id': contract.id,
641 'auto_generated': True
643 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
644 startdate += deltas.get(contract.cost_frequency)
647 def scheduler_manage_contract_expiration(self, cr, uid, context=None):
648 #This method is called by a cron task
649 #It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
650 datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
651 limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
652 ids = self.search(cr, uid, ['&', ('state', '=', 'open'), ('expiration_date', '<', limit_date)], offset=0, limit=None, order=None, context=context, count=False)
654 for contract in self.browse(cr, uid, ids, context=context):
655 if contract.vehicle_id.id in res:
656 res[contract.vehicle_id.id] += 1
658 res[contract.vehicle_id.id] = 1
660 for vehicle, value in res.items():
661 self.pool.get('fleet.vehicle').message_post(cr, uid, vehicle, body=_('%s contract(s) need(s) to be renewed and/or closed!') % (str(value)), context=context)
662 return self.write(cr, uid, ids, {'state': 'toclose'}, context=context)
664 def run_scheduler(self, cr, uid, context=None):
665 self.scheduler_manage_auto_costs(cr, uid, context=context)
666 self.scheduler_manage_contract_expiration(cr, uid, context=context)
669 def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
671 for record in self.browse(cr, uid, ids, context=context):
672 name = record.vehicle_id.name
673 if record.cost_subtype_id.name:
674 name += ' / '+ record.cost_subtype_id.name
676 name += ' / '+ record.date
677 res[record.id] = name
680 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
683 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
686 'odometer_unit': odometer_unit,
690 def compute_next_year_date(self, strdate):
691 oneyear = datetime.timedelta(days=365)
692 curdate = str_to_datetime(strdate)
693 return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
695 def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
697 return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
700 def get_days_left(self, cr, uid, ids, prop, unknow_none, context=None):
701 """return a dict with as value for each contract an integer
702 if contract is in an open state and is overdue, return 0
703 if contract is in a closed state, return -1
704 otherwise return the number of days before the contract expires
707 for record in self.browse(cr, uid, ids, context=context):
708 if (record.expiration_date and (record.state == 'open' or record.state == 'toclose')):
709 today = str_to_datetime(time.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
710 renew_date = str_to_datetime(record.expiration_date)
711 diff_time = (renew_date-today).days
712 res[record.id] = diff_time > 0 and diff_time or 0
717 def act_renew_contract(self, cr, uid, ids, context=None):
718 assert len(ids) == 1, "This operation should only be done for 1 single contract at a time, as it it suppose to open a window as result"
719 for element in self.browse(cr, uid, ids, context=context):
721 startdate = str_to_datetime(element.start_date)
722 enddate = str_to_datetime(element.expiration_date)
723 diffdate = (enddate - startdate)
725 'date': fields.date.context_today(self, cr, uid, context=context),
726 'start_date': datetime.datetime.strftime(str_to_datetime(element.expiration_date) + datetime.timedelta(days=1), tools.DEFAULT_SERVER_DATE_FORMAT),
727 'expiration_date': datetime.datetime.strftime(enddate + diffdate, tools.DEFAULT_SERVER_DATE_FORMAT),
729 newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, element.id, default, context=context)
730 mod, modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
732 'name':_("Renew Contract"),
735 'view_type': 'tree,form',
736 'res_model': 'fleet.vehicle.log.contract',
737 'type': 'ir.actions.act_window',
741 'context': {'active_id':newid},
744 def _get_default_contract_type(self, cr, uid, context=None):
746 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
751 def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
753 for element in cost_ids:
754 if element and len(element) == 3 and isinstance(element[2], dict):
755 totalsum += element[2].get('amount', 0.0)
758 'sum_cost': totalsum,
762 def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
764 for contract in self.browse(cr, uid, ids, context=context):
766 for cost in contract.cost_ids:
767 totalsum += cost.amount
768 res[contract.id] = totalsum
771 _inherits = {'fleet.vehicle.cost': 'cost_id'}
772 _name = 'fleet.vehicle.log.contract'
773 _description = 'Contract information on a vehicle'
774 _order='state desc,expiration_date'
776 'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
777 'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
778 'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
779 'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
780 'insurer_id' :fields.many2one('res.partner', 'Supplier'),
781 'purchaser_id': fields.many2one('res.partner', 'Contractor', help='Person to which the contract is signed for'),
782 'ins_ref': fields.char('Contract Reference', size=64, copy=False),
783 'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')],
784 'Status', readonly=True, help='Choose wheter the contract is still valid or not',
786 'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract', copy=False),
787 'cost_generated': fields.float('Recurring Cost Amount', help="Costs paid at regular intervals, depending on the cost frequency. If the cost frequency is set to unique, the cost will be logged at the start date"),
788 'cost_frequency': fields.selection([('no','No'), ('daily', 'Daily'), ('weekly','Weekly'), ('monthly','Monthly'), ('yearly','Yearly')], 'Recurring Cost Frequency', help='Frequency of the recuring cost', required=True),
789 'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs'),
790 'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
791 'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade'),
792 'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
795 'purchaser_id': lambda self, cr, uid, ctx: self.pool.get('res.users').browse(cr, uid, uid, context=ctx).partner_id.id or False,
796 'date': fields.date.context_today,
797 'start_date': fields.date.context_today,
799 'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
800 'cost_frequency': 'no',
801 'cost_subtype_id': _get_default_contract_type,
802 'cost_type': 'contract',
805 def contract_close(self, cr, uid, ids, context=None):
806 return self.write(cr, uid, ids, {'state': 'closed'}, context=context)
808 def contract_open(self, cr, uid, ids, context=None):
809 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
811 class fleet_contract_state(osv.Model):
812 _name = 'fleet.contract.state'
813 _description = 'Contains the different possible status of a leasing contract'
816 'name':fields.char('Contract Status', required=True),