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)
56 def _year_get_fnc(self, cr, uid, ids, name, unknow_none, context=None):
58 for record in self.browse(cr, uid, ids, context=context):
60 res[record.id] = str(time.strptime(record.date, tools.DEFAULT_SERVER_DATE_FORMAT).tm_year)
62 res[record.id] = _('Unknown')
65 def _cost_name_get_fnc(self, cr, uid, ids, name, unknow_none, context=None):
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
72 name += ' / '+ record.date
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),
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)
116 class fleet_vehicle_tag(osv.Model):
117 _name = 'fleet.vehicle.tag'
119 'name': fields.char('Name', required=True, translate=True),
122 class fleet_vehicle_state(osv.Model):
123 _name = 'fleet.vehicle.state'
124 _order = 'sequence asc'
126 'name': fields.char('Name', required=True),
127 'sequence': fields.integer('Sequence', help="Used to order the note stages")
129 _sql_constraints = [('fleet_state_name_unique','unique(name)', 'State name already exists')]
132 class fleet_vehicle_model(osv.Model):
134 def _model_name_get_fnc(self, cr, uid, ids, field_name, arg, context=None):
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
143 def on_change_brand(self, cr, uid, ids, model_id, context=None):
145 return {'value': {'image_medium': False}}
146 brand = self.pool.get('fleet.vehicle.model.brand').browse(cr, uid, model_id, context=context)
149 'image_medium': brand.image,
153 _name = 'fleet.vehicle.model'
154 _description = 'Model of a vehicle'
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"),
168 class fleet_vehicle_model_brand(osv.Model):
169 _name = 'fleet.vehicle.model.brand'
170 _description = 'Brand model of the vehicle'
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)
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)
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",
190 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
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",
198 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
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."),
206 class fleet_vehicle(osv.Model):
208 _inherit = 'mail.thread'
210 def _vehicle_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
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
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 """
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])]
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
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
240 res['domain'] = [('vehicle_id','=', ids[0])]
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')
248 res[record.id] = self.pool.get('fleet.vehicle.odometer').browse(cr, uid, ids[0], context=context).value
251 def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
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)
257 def _search_get_overdue_contract_reminder(self, cr, uid, obj, name, args, context):
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'
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))
271 def _search_contract_renewal_due_soon(self, cr, uid, obj, name, args, context):
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'
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))
287 def _get_contract_reminder_fnc(self, cr, uid, ids, field_names, unknow_none, context=None):
289 for record in self.browse(cr, uid, ids, context=context):
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
304 if diff_time < 15 and diff_time >= 0:
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')
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)
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,
321 def _get_default_state(self, cr, uid, context):
323 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'vehicle_state_active')
328 _name = 'fleet.vehicle'
329 _description = 'Information on a vehicle'
330 _order= 'license_plate asc'
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'),
368 'odometer_unit': 'kilometers',
369 'state_id': _get_default_state,
372 def copy(self, cr, uid, id, default=None, context=None):
382 return super(fleet_vehicle, self).copy(cr, uid, id, default, context=context)
384 def on_change_model(self, cr, uid, ids, model_id, context=None):
387 model = self.pool.get('fleet.vehicle.model').browse(cr, uid, model_id, context=context)
390 'image_medium': model.image,
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)
400 def write(self, cr, uid, ids, vals, context=None):
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
405 for vehicle in self.browse(cr, uid, ids, context):
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']))
424 self.message_post(cr, uid, [vehicle.id], body=", ".join(changes), context=context)
426 vehicle_id = super(fleet_vehicle,self).write(cr, uid, ids, vals, context)
430 class fleet_vehicle_odometer(osv.Model):
431 _name='fleet.vehicle.odometer'
432 _description='Odometer log for a vehicle'
435 def _vehicle_log_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
437 for record in self.browse(cr, uid, ids, context=context):
438 name = record.vehicle_id.name
440 name = name+ ' / '+ str(record.date)
441 res[record.id] = name
444 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
447 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
450 'unit': odometer_unit,
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),
462 'date': fields.date.context_today,
466 class fleet_vehicle_log_fuel(osv.Model):
468 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
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
476 'odometer_unit': odometer_unit,
477 'purchaser_id': driver,
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
486 #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
487 #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
488 #computation to 2 decimal
490 price_per_liter = float(price_per_liter)
491 amount = float(amount)
492 if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
493 return {'value' : {'amount' : round(liter * price_per_liter,2),}}
494 elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
495 return {'value' : {'price_per_liter' : round(amount / liter,2),}}
496 elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
497 return {'value' : {'liter' : round(amount / price_per_liter,2),}}
501 def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
502 #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
503 #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
504 #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
506 #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
507 #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
508 #computation to 2 decimal
510 price_per_liter = float(price_per_liter)
511 amount = float(amount)
512 if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
513 return {'value' : {'amount' : round(liter * price_per_liter,2),}}
514 elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
515 return {'value' : {'liter' : round(amount / price_per_liter,2),}}
516 elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
517 return {'value' : {'price_per_liter' : round(amount / liter,2),}}
521 def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
522 #need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
523 #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
524 #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
526 #If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
527 #onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
528 #computation to 2 decimal
530 price_per_liter = float(price_per_liter)
531 amount = float(amount)
532 if amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
533 return {'value': {'price_per_liter': round(amount / liter,2),}}
534 elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
535 return {'value': {'liter': round(amount / price_per_liter,2),}}
536 elif liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
537 return {'value': {'amount': round(liter * price_per_liter,2),}}
541 def _get_default_service_type(self, cr, uid, context):
543 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
548 _name = 'fleet.vehicle.log.fuel'
549 _description = 'Fuel log for vehicles'
550 _inherits = {'fleet.vehicle.cost': 'cost_id'}
553 'liter': fields.float('Liter'),
554 'price_per_liter': fields.float('Price Per Liter'),
555 'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
556 'inv_ref': fields.char('Invoice Reference', size=64),
557 'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
558 'notes': fields.text('Notes'),
559 'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost'),
560 '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
563 'date': fields.date.context_today,
564 'cost_subtype_id': _get_default_service_type,
569 class fleet_vehicle_log_services(osv.Model):
571 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
574 vehicle = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context)
575 odometer_unit = vehicle.odometer_unit
576 driver = vehicle.driver_id.id
579 'odometer_unit': odometer_unit,
580 'purchaser_id': driver,
584 def _get_default_service_type(self, cr, uid, context):
586 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
591 _inherits = {'fleet.vehicle.cost': 'cost_id'}
592 _name = 'fleet.vehicle.log.services'
593 _description = 'Services for vehicles'
595 'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
596 'inv_ref': fields.char('Invoice Reference', size=64),
597 'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
598 '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
599 'notes': fields.text('Notes'),
600 'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost'),
603 'date': fields.date.context_today,
604 'cost_subtype_id': _get_default_service_type,
605 'cost_type': 'services'
609 class fleet_service_type(osv.Model):
610 _name = 'fleet.service.type'
611 _description = 'Type of services available on a vehicle'
613 'name': fields.char('Name', required=True, translate=True),
614 'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
618 class fleet_vehicle_log_contract(osv.Model):
620 def scheduler_manage_auto_costs(self, cr, uid, context=None):
621 #This method is called by a cron task
622 #It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
623 #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
624 #If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
625 #The created costs are associated to a contract thanks to the many2one field contract_id
626 #If the contract has no start_date, no cost will be created, even if the contract has recurring costs
627 vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
628 d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
629 contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
630 deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
631 for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
632 if not contract.start_date or contract.cost_frequency == 'no':
635 last_cost_date = contract.start_date
636 if contract.generated_cost_ids:
637 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)
638 if last_autogenerated_cost_id:
640 last_cost_date = vehicle_cost_obj.browse(cr, uid, last_autogenerated_cost_id[0], context=context).date
641 startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
643 startdate += deltas.get(contract.cost_frequency)
644 while (startdate <= d) & (startdate <= datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
646 'amount': contract.cost_generated,
647 'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
648 'vehicle_id': contract.vehicle_id.id,
649 'cost_subtype_id': contract.cost_subtype_id.id,
650 'contract_id': contract.id,
651 'auto_generated': True
653 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
654 startdate += deltas.get(contract.cost_frequency)
657 def scheduler_manage_contract_expiration(self, cr, uid, context=None):
658 #This method is called by a cron task
659 #It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
660 datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
661 limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
662 ids = self.search(cr, uid, ['&', ('state', '=', 'open'), ('expiration_date', '<', limit_date)], offset=0, limit=None, order=None, context=context, count=False)
664 for contract in self.browse(cr, uid, ids, context=context):
665 if contract.vehicle_id.id in res:
666 res[contract.vehicle_id.id] += 1
668 res[contract.vehicle_id.id] = 1
670 for vehicle, value in res.items():
671 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)
672 return self.write(cr, uid, ids, {'state': 'toclose'}, context=context)
674 def run_scheduler(self, cr, uid, context=None):
675 self.scheduler_manage_auto_costs(cr, uid, context=context)
676 self.scheduler_manage_contract_expiration(cr, uid, context=context)
679 def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
681 for record in self.browse(cr, uid, ids, context=context):
682 name = record.vehicle_id.name
683 if record.cost_subtype_id.name:
684 name += ' / '+ record.cost_subtype_id.name
686 name += ' / '+ record.date
687 res[record.id] = name
690 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
693 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
696 'odometer_unit': odometer_unit,
700 def compute_next_year_date(self, strdate):
701 oneyear = datetime.timedelta(days=365)
702 curdate = str_to_datetime(strdate)
703 return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
705 def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
707 return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
710 def get_days_left(self, cr, uid, ids, prop, unknow_none, context=None):
711 """return a dict with as value for each contract an integer
712 if contract is in an open state and is overdue, return 0
713 if contract is in a closed state, return -1
714 otherwise return the number of days before the contract expires
717 for record in self.browse(cr, uid, ids, context=context):
718 if (record.expiration_date and (record.state == 'open' or record.state == 'toclose')):
719 today = str_to_datetime(time.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
720 renew_date = str_to_datetime(record.expiration_date)
721 diff_time = (renew_date-today).days
722 res[record.id] = diff_time > 0 and diff_time or 0
727 def act_renew_contract(self, cr, uid, ids, context=None):
728 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"
729 for element in self.browse(cr, uid, ids, context=context):
731 startdate = str_to_datetime(element.start_date)
732 enddate = str_to_datetime(element.expiration_date)
733 diffdate = (enddate - startdate)
735 'date': fields.date.context_today(self, cr, uid, context=context),
736 'start_date': datetime.datetime.strftime(str_to_datetime(element.expiration_date) + datetime.timedelta(days=1), tools.DEFAULT_SERVER_DATE_FORMAT),
737 'expiration_date': datetime.datetime.strftime(enddate + diffdate, tools.DEFAULT_SERVER_DATE_FORMAT),
739 newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, element.id, default, context=context)
740 mod, modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
742 'name':_("Renew Contract"),
745 'view_type': 'tree,form',
746 'res_model': 'fleet.vehicle.log.contract',
747 'type': 'ir.actions.act_window',
751 'context': {'active_id':newid},
754 def _get_default_contract_type(self, cr, uid, context=None):
756 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
761 def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
763 for element in cost_ids:
764 if element and len(element) == 3 and element[2] is not False:
765 totalsum += element[2].get('amount', 0.0)
768 'sum_cost': totalsum,
772 def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
774 for contract in self.browse(cr, uid, ids, context=context):
776 for cost in contract.cost_ids:
777 totalsum += cost.amount
778 res[contract.id] = totalsum
781 _inherits = {'fleet.vehicle.cost': 'cost_id'}
782 _name = 'fleet.vehicle.log.contract'
783 _description = 'Contract information on a vehicle'
784 _order='state desc,expiration_date'
786 'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
787 'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
788 'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
789 'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
790 'insurer_id' :fields.many2one('res.partner', 'Supplier'),
791 'purchaser_id': fields.many2one('res.partner', 'Contractor', help='Person to which the contract is signed for'),
792 'ins_ref': fields.char('Contract Reference', size=64),
793 'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')], 'Status', readonly=True, help='Choose wheter the contract is still valid or not'),
794 'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract'),
795 '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"),
796 '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),
797 'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs', ondelete='cascade'),
798 'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
799 'cost_id': fields.many2one('fleet.vehicle.cost', 'Cost'),
800 '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
803 'purchaser_id': lambda self, cr, uid, ctx: self.pool.get('res.users').browse(cr, uid, uid, context=ctx).partner_id.id or False,
804 'date': fields.date.context_today,
805 'start_date': fields.date.context_today,
807 'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
808 'cost_frequency': 'no',
809 'cost_subtype_id': _get_default_contract_type,
810 'cost_type': 'contract',
813 def copy(self, cr, uid, id, default=None, context=None):
816 today = fields.date.context_today(self, cr, uid, context=context)
817 default['date'] = today
818 default['start_date'] = today
819 default['expiration_date'] = self.compute_next_year_date(today)
820 default['ins_ref'] = ''
821 default['state'] = 'open'
822 default['notes'] = ''
823 return super(fleet_vehicle_log_contract, self).copy(cr, uid, id, default, context=context)
825 def contract_close(self, cr, uid, ids, context=None):
826 return self.write(cr, uid, ids, {'state': 'closed'}, context=context)
828 def contract_open(self, cr, uid, ids, context=None):
829 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
831 class fleet_contract_state(osv.Model):
832 _name = 'fleet.contract.state'
833 _description = 'Contains the different possible status of a leasing contract'
836 'name':fields.char('Contract Status', size=64, required=True),