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 osv import osv, fields
26 from osv.orm import except_orm
27 from tools.translate import _
28 from dateutil.relativedelta import relativedelta
30 def str_to_date(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):
59 res[record.id] = str(time.strptime(record.date, tools.DEFAULT_SERVER_DATE_FORMAT).tm_year)
62 def _cost_name_get_fnc(self, cr, uid, ids, name, unknow_none, context=None):
64 for record in self.browse(cr, uid, ids, context=context):
65 name = record.vehicle_id.name
66 if record.cost_subtype.name:
67 name += ' / '+ record.cost_subtype.name
69 name += ' / '+ record.date
74 'name': fields.function(_cost_name_get_fnc, type="char", string='Name', store=True),
75 'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True, help='Vehicle concerned by this log'),
76 'cost_subtype': fields.many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost'),
77 'amount': fields.float('Total Price'),
78 'cost_type': fields.selection([('contract', 'Contract'), ('services','Services'), ('fuel','Fuel'), ('other','Other')], 'Category of the cost', help='For internal purpose only', required=True),
79 'parent_id': fields.many2one('fleet.vehicle.cost', 'Parent', help='Parent cost to this current cost'),
80 'cost_ids': fields.one2many('fleet.vehicle.cost', 'parent_id', 'Included Services'),
81 'odometer_id': fields.many2one('fleet.vehicle.odometer', 'Odometer', help='Odometer measure of the vehicle at the moment of this log'),
82 'odometer': fields.function(_get_odometer, fnct_inv=_set_odometer, type='float', string='Odometer Value', help='Odometer measure of the vehicle at the moment of this log'),
83 'odometer_unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
84 'date' :fields.date('Date',help='Date when the cost has been executed'),
85 'contract_id': fields.many2one('fleet.vehicle.log.contract', 'Contract', help='Contract attached to this cost'),
86 'auto_generated': fields.boolean('Automatically Generated', readonly=True, required=True),
87 'year': fields.function(_year_get_fnc, type="char", string='Year', store=True),
94 def create(self, cr, uid, data, context=None):
95 if 'parent_id' in data and data['parent_id']:
96 parent = self.browse(cr, uid, data['parent_id'], context=context)
97 data['vehicle_id'] = parent.vehicle_id.id
98 data['date'] = parent.date
99 data['cost_type'] = parent.cost_type
100 if 'contract_id' in data and data['contract_id']:
101 contract = self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, data['contract_id'], context=context)
102 data['vehicle_id'] = contract.vehicle_id.id
103 data['cost_subtype'] = contract.cost_subtype.id
104 data['cost_type'] = contract.cost_type
105 if 'odometer' in data and not data['odometer']:
106 del(data['odometer'])
107 return super(fleet_vehicle_cost, self).create(cr, uid, data, context=context)
110 class fleet_vehicle_tag(osv.Model):
111 _name = 'fleet.vehicle.tag'
113 'name': fields.char('Name', required=True, translate=True),
116 class fleet_vehicle_state(osv.Model):
117 _name = 'fleet.vehicle.state'
118 _order = 'sequence asc'
120 'name': fields.char('Name', required=True),
121 'sequence': fields.integer('Order', help="Used to order the note stages")
123 _sql_constraints = [('fleet_state_name_unique','unique(name)', 'State name already exists')]
126 class fleet_vehicle_model(osv.Model):
128 def _model_name_get_fnc(self, cr, uid, ids, field_name, arg, context=None):
130 for record in self.browse(cr, uid, ids, context=context):
131 name = record.modelname
132 if record.brand.name:
133 name = record.brand.name + ' / ' + name
134 res[record.id] = name
137 def on_change_brand(self, cr, uid, ids, model_id, context=None):
139 return {'value': {'image_medium': False}}
140 brand = self.pool.get('fleet.vehicle.model.brand').browse(cr, uid, model_id, context=context)
143 'image_medium': brand.image,
147 _name = 'fleet.vehicle.model'
148 _description = 'Model of a vehicle'
152 'name': fields.function(_model_name_get_fnc, type="char", string='Name', store=True),
153 'modelname': fields.char('Model name', size=32, required=True),
154 'brand': fields.many2one('fleet.vehicle.model.brand', 'Model Brand', required=True, help='Brand of the vehicle'),
155 'vendors': fields.many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors'),
156 'image': fields.related('brand', 'image', type="binary", string="Logo"),
157 'image_medium': fields.related('brand', 'image_medium', type="binary", string="Logo"),
158 'image_small': fields.related('brand', 'image_small', type="binary", string="Logo"),
162 class fleet_vehicle_model_brand(osv.Model):
163 _name = 'fleet.vehicle.model.brand'
164 _description = 'Brand model of the vehicle'
168 def _get_image(self, cr, uid, ids, name, args, context=None):
169 result = dict.fromkeys(ids, False)
170 for obj in self.browse(cr, uid, ids, context=context):
171 result[obj.id] = tools.image_get_resized_images(obj.image)
174 def _set_image(self, cr, uid, id, name, value, args, context=None):
175 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
178 'name': fields.char('Brand Name', size=64, required=True),
179 'image': fields.binary("Logo",
180 help="This field holds the image used as logo for the brand, limited to 1024x1024px."),
181 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
182 string="Medium-sized photo", type="binary", multi="_get_image",
184 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
186 help="Medium-sized logo of the brand. It is automatically "\
187 "resized as a 128x128px image, with aspect ratio preserved. "\
188 "Use this field in form views or some kanban views."),
189 'image_small': fields.function(_get_image, fnct_inv=_set_image,
190 string="Smal-sized photo", type="binary", multi="_get_image",
192 'fleet.vehicle.model.brand': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
194 help="Small-sized photo of the brand. It is automatically "\
195 "resized as a 64x64px image, with aspect ratio preserved. "\
196 "Use this field anywhere a small image is required."),
200 class fleet_vehicle(osv.Model):
202 _inherit = 'mail.thread'
204 def _vehicle_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
206 for record in self.browse(cr, uid, ids, context=context):
207 res[record.id] = record.model_id.brand.name + '/' + record.model_id.modelname + ' / ' + record.license_plate
210 def return_action_to_open(self, cr, uid, ids, context=None):
211 """ This opens the xml view specified in xml_id for the current vehicle """
214 if context.get('xml_id'):
215 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet', context['xml_id'], context=context)
216 res['context'] = context
217 res['context'].update({'default_vehicle_id': ids[0]})
218 res['domain'] = [('vehicle_id','=', ids[0])]
222 def act_show_log_cost(self, cr, uid, ids, context=None):
223 """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
224 @return: the costs log view
228 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet','fleet_vehicle_costs_act', context=context)
229 res['context'] = context
230 res['context'].update({
231 'default_vehicle_id': ids[0],
232 'search_default_parent_false': True
234 res['domain'] = [('vehicle_id','=', ids[0])]
237 def _get_odometer(self, cr, uid, ids, odometer_id, arg, context):
238 res = dict.fromkeys(ids, 0)
239 for record in self.browse(cr,uid,ids,context=context):
240 ids = self.pool.get('fleet.vehicle.odometer').search(cr, uid, [('vehicle_id', '=', record.id)], limit=1, order='value desc')
242 res[record.id] = self.pool.get('fleet.vehicle.odometer').browse(cr, uid, ids[0], context=context).value
245 def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
247 date = fields.date.context_today(self, cr, uid, context=context)
248 data = {'value': value, 'date': date, 'vehicle_id': id}
249 return self.pool.get('fleet.vehicle.odometer').create(cr, uid, data, context=context)
251 def _search_get_overdue_contract_reminder(self, cr, uid, obj, name, args, context):
253 today = fields.date.today(self, cr, uid, context=context)
254 for field, operator, value in args:
255 assert operator == '>' and value == 0, 'Operation not supported'
256 today = fields.date.context_today(self, cr, uid, context=context)
257 cr.execute('select cost.vehicle_id, count(contract.id) as contract_number FROM fleet_vehicle_cost cost left join fleet_vehicle_log_contract contract on contract.cost_id = cost.id WHERE contract.expiration_date is not null AND contract.expiration_date < %s AND contract.state IN (\'open\', \'toclose\') GROUP BY cost.vehicle_id', (today,))
258 res_ids = [x[0] for x in cr.fetchall()]
259 res.append(('id', 'in', res_ids))
262 def _search_contract_renewal_due_soon(self, cr, uid, obj, name, args, context):
264 for field, operator, value in args:
265 assert operator == '>' and value == 0, 'Operation not supported'
266 today = fields.date.context_today(self, cr, uid, context=context)
267 datetime_today = datetime.datetime.strptime(today, tools.DEFAULT_SERVER_DATE_FORMAT)
268 limit_date = str((datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
269 cr.execute('select cost.vehicle_id, count(contract.id) as contract_number FROM fleet_vehicle_cost cost left join fleet_vehicle_log_contract contract on contract.cost_id = cost.id WHERE contract.expiration_date is not null AND contract.expiration_date > %s AND contract.expiration_date < %s AND contract.state IN (\'open\', \'toclose\') GROUP BY cost.vehicle_id', (today, limit_date))
270 res_ids = [x[0] for x in cr.fetchall()]
271 res.append(('id', 'in', res_ids))
274 def _get_contract_reminder_fnc(self, cr, uid, ids, field_names, unknow_none, context=None):
276 for record in self.browse(cr, uid, ids, context=context):
281 for element in record.log_contracts:
282 if element.state in ('open', 'toclose') and element.expiration_date:
283 current_date_str = fields.date.context_today(self, cr, uid, context=context)
284 due_time_str = element.expiration_date
285 current_date = str_to_date(current_date_str)
286 due_time = str_to_date(due_time_str)
287 diff_time = (due_time-current_date).days
291 if diff_time<15 and diff_time>=0:
294 if overdue or due_soon:
295 ids = self.pool.get('fleet.vehicle.log.contract').search(cr,uid,[('vehicle_id', '=', record.id), ('state', 'in', ('open', 'toclose'))], limit=1, order='expiration_date asc')
297 #we display only the name of the oldest overdue/due soon contract
298 name=(self.pool.get('fleet.vehicle.log.contract').browse(cr,uid,ids[0],context=context).cost_subtype.name)
301 'contract_renewal_overdue': overdue,
302 'contract_renewal_due_soon': due_soon,
303 'contract_renewal_total': (total - 1), #we remove 1 from the real total for display purposes
304 'contract_renewal_name': name,
308 def run_scheduler(self, cr, uid, context=None):
309 datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
310 limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
311 ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, ['&', ('state', '=', 'open'), ('expiration_date', '<', limit_date)], offset=0, limit=None, order=None, context=context, count=False)
313 for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, ids, context=context):
314 if contract.vehicle_id.id in res:
315 res[contract.vehicle_id.id] += 1
317 res[contract.vehicle_id.id] = 1
319 for vehicle, value in res.items():
320 self.message_post(cr, uid, vehicle, body=_('%s contract(s) need(s) to be renewed and/or closed!') % (str(value)), context=context)
322 return self.pool.get('fleet.vehicle.log.contract').write(cr, uid, ids, {'state': 'toclose'}, context=context)
324 def _get_default_state(self, cr, uid, context):
326 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'vehicle_state_active')
331 _name = 'fleet.vehicle'
332 _description = 'Information on a vehicle'
333 _order= 'license_plate asc'
335 'name': fields.function(_vehicle_name_get_fnc, type="char", string='Name', store=True),
336 'company_id': fields.many2one('res.company', 'Company'),
337 'license_plate': fields.char('License Plate', size=32, required=True, help='License plate number of the vehicle (ie: plate number for a car)'),
338 'vin_sn': fields.char('Chassis Number', size=32, help='Unique number written on the vehicle motor (VIN/SN number)'),
339 'driver': fields.many2one('res.partner', 'Driver', help='Driver of the vehicle'),
340 'model_id': fields.many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle'),
341 'log_fuel': fields.one2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs'),
342 'log_services': fields.one2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs'),
343 'log_contracts': fields.one2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts'),
344 'acquisition_date': fields.date('Acquisition Date', required=False, help='Date when the vehicle has been bought'),
345 'color': fields.char('Color', size=32, help='Color of the vehicle'),
346 'state': fields.many2one('fleet.vehicle.state', 'State', help='Current state of the vehicle', ondelete="set null"),
347 'location': fields.char('Location', size=128, help='Location of the vehicle (garage, ...)'),
348 'seats': fields.integer('Seats Number', help='Number of seats of the vehicle'),
349 'doors': fields.integer('Doors Number', help='Number of doors of the vehicle'),
350 'tag_ids' :fields.many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id','tag_id', 'Tags'),
351 'odometer': fields.function(_get_odometer, fnct_inv=_set_odometer, type='float', string='Odometer Value', help='Odometer measure of the vehicle at the moment of this log'),
352 'odometer_unit': fields.selection([('kilometers', 'Kilometers'),('miles','Miles')], 'Odometer Unit', help='Unit of the odometer ',required=True),
353 'transmission': fields.selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle'),
354 'fuel_type': fields.selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle'),
355 'horsepower': fields.integer('Horsepower'),
356 'horsepower_tax': fields.float('Horsepower Taxation'),
357 'power': fields.integer('Power (kW)', help='Power in kW of the vehicle'),
358 'co2': fields.float('CO2 Emissions', help='CO2 emissions of the vehicle'),
359 'image': fields.related('model_id', 'image', type="binary", string="Logo"),
360 'image_medium': fields.related('model_id', 'image_medium', type="binary", string="Logo"),
361 'image_small': fields.related('model_id', 'image_small', type="binary", string="Logo"),
362 'contract_renewal_due_soon': fields.function(_get_contract_reminder_fnc, fnct_search=_search_contract_renewal_due_soon, type="boolean", string='Contracts to renew', multi='contract_info'),
363 'contract_renewal_overdue': fields.function(_get_contract_reminder_fnc, fnct_search=_search_get_overdue_contract_reminder, type="boolean", string='Contracts Overdued', multi='contract_info'),
364 'contract_renewal_name': fields.function(_get_contract_reminder_fnc, type="text", string='Name of contract to renew soon', multi='contract_info'),
365 'contract_renewal_total': fields.function(_get_contract_reminder_fnc, type="integer", string='Total of contracts due or overdue minus one', multi='contract_info'),
366 'car_value': fields.float('Car Value', help='Value of the bought vehicle'),
371 'odometer_unit': 'kilometers',
372 'state': _get_default_state,
375 def copy(self, cr, uid, id, default=None, context=None):
385 return super(fleet_vehicle, self).copy(cr, uid, id, default, context=context)
387 def on_change_model(self, cr, uid, ids, model_id, context=None):
390 model = self.pool.get('fleet.vehicle.model').browse(cr, uid, model_id, context=context)
393 'image_medium': model.image,
397 def create(self, cr, uid, data, context=None):
398 vehicle_id = super(fleet_vehicle, self).create(cr, uid, data, context=context)
399 vehicle = self.browse(cr, uid, vehicle_id, context=context)
400 self.message_post(cr, uid, [vehicle_id], body=_('Vehicle %s has been added to the fleet!') % (vehicle.license_plate), context=context)
403 def write(self, cr, uid, ids, vals, context=None):
405 This function write an entry in the openchatter whenever we change important information
406 on the vehicle like the model, the drive, the state of the vehicle or its license plate
408 for vehicle in self.browse(cr, uid, ids, context):
410 if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
411 value = self.pool.get('fleet.vehicle.model').browse(cr,uid,vals['model_id'],context=context).name
412 oldmodel = vehicle.model_id.name or _('None')
413 changes.append(_('Model: from \' %s \' to \' %s \'') %(oldmodel, value))
414 if 'driver' in vals and vehicle.driver.id != vals['driver']:
415 value = self.pool.get('res.partner').browse(cr,uid,vals['driver'],context=context).name
416 olddriver = (vehicle.driver.name) or _('None')
417 changes.append(_('Driver: from \' %s \' to \' %s \'') %(olddriver, value))
418 if 'state' in vals and vehicle.state.id != vals['state']:
419 value = self.pool.get('fleet.vehicle.state').browse(cr,uid,vals['state'],context=context).name
420 oldstate = vehicle.state.name or _('None')
421 changes.append(_('State: from \' %s \' to \' %s \'') %(oldstate, value))
422 if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
423 old_license_plate = vehicle.license_plate or _('None')
424 changes.append(_('License Plate: from \' %s \' to \' %s \'') %(old_license_plate, vals['license_plate']))
427 self.message_post(cr, uid, [vehicle.id], body=", ".join(changes), context=context)
429 vehicle_id = super(fleet_vehicle,self).write(cr, uid, ids, vals, context)
433 class fleet_vehicle_odometer(osv.Model):
434 _name='fleet.vehicle.odometer'
435 _description='Odometer log for a vehicle'
438 def _vehicle_log_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
440 for record in self.browse(cr, uid, ids, context=context):
441 name = record.vehicle_id.name
443 name = name+ ' / '+ str(record.date)
444 res[record.id] = name
447 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
450 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
453 'unit': odometer_unit,
458 'name': fields.function(_vehicle_log_name_get_fnc, type="char", string='Name', store=True),
459 'date': fields.date('Date'),
460 'value': fields.float('Odometer Value', group_operator="max"),
461 'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True),
462 'unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
465 'date': fields.date.context_today,
469 class fleet_vehicle_log_fuel(osv.Model):
471 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
474 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
477 'odometer_unit': odometer_unit,
481 def on_change_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
482 #need to cast in float because the value returned by onchange isn't automatically cast into float, so 3 is consider as an integer
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,}}
495 def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
496 #need to cast in float because the value returned by onchange isn't automatically cast into float, so 3 is consider as an integer
498 price_per_liter = float(price_per_liter)
499 amount = float(amount)
500 if price_per_liter > 0 and liter > 0:
501 return {'value' : {'amount' : liter * price_per_liter,}}
502 elif price_per_liter > 0 and amount > 0:
503 return {'value' : {'liter' : amount / price_per_liter,}}
504 elif liter > 0 and amount > 0:
505 return {'value' : {'price_per_liter' : amount / liter,}}
509 def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
510 #need to cast in float because the value returned by onchange isn't automatically cast into float, so 3 is consider as an integer
512 price_per_liter = float(price_per_liter)
513 amount = float(amount)
514 if amount > 0 and liter > 0:
515 return {'value': {'price_per_liter': amount / liter,}}
516 elif amount > 0 and price_per_liter > 0:
517 return {'value': {'liter': amount / price_per_liter,}}
518 elif liter > 0 and price_per_liter > 0:
519 return {'value': {'amount': liter * price_per_liter,}}
522 def _get_default_service_type(self, cr, uid, context):
524 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
529 _name = 'fleet.vehicle.log.fuel'
530 _description = 'Fuel log for vehicles'
531 _inherits = {'fleet.vehicle.cost': 'cost_id'}
534 'liter': fields.float('Liter'),
535 'price_per_liter': fields.float('Price Per Liter'),
536 'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
537 'inv_ref': fields.char('Invoice Reference', size=64),
538 'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
539 'notes': fields.text('Notes'),
540 'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
543 'purchaser_id': lambda self, cr, uid, ctx: uid,
544 'date': fields.date.context_today,
545 'cost_subtype': _get_default_service_type,
550 class fleet_vehicle_log_services(osv.Model):
552 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
555 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
558 'odometer_unit': odometer_unit,
562 def _get_default_service_type(self, cr, uid, context):
564 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
569 _inherits = {'fleet.vehicle.cost': 'cost_id'}
570 _name = 'fleet.vehicle.log.services'
571 _description = 'Services for vehicles'
573 'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
574 'inv_ref': fields.char('Invoice Reference', size=64),
575 'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
576 'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
577 'notes': fields.text('Notes'),
580 'purchaser_id': lambda self, cr, uid, ctx: uid,
581 'date': fields.date.context_today,
582 'cost_subtype': _get_default_service_type,
583 'cost_type': 'services'
587 class fleet_service_type(osv.Model):
588 _name = 'fleet.service.type'
589 _description = 'Type of services available on a vehicle'
591 'name': fields.char('Name', required=True, translate=True),
592 'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
596 class fleet_vehicle_log_contract(osv.Model):
598 def run_scheduler(self,cr,uid,context=None):
599 #This method is called by a cron task
600 #It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
601 #For example, if a contract has a reccuring cost of 200 with a weekly frequency, this method creates a cost of 200 on the first day of each week, from the date of the last recurring costs in the database to today
602 #If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
603 #The created costs are associated to a contract thanks to the many2one field contract_id
604 #If the contract has no start_date, no cost will be created, even if the contract has recurring costs
605 vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
606 d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
607 contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
608 deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
609 for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
610 if not contract.start_date or contract.cost_frequency == 'no':
613 last_cost_date = contract.start_date
614 if contract.generated_cost_ids:
615 last_autogenerated_cost_id = vehicle_cost_obj.search(cr, uid, ['&', ('contract_id','=',contract.id), ('auto_generated','=',True)], offset=0, limit=1, order='date desc',context=context, count=False)
616 if last_autogenerated_cost_id:
618 last_cost_date = vehicle_cost_obj.browse(cr, uid, last_cost_id[0], context=context).date
619 startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
621 startdate += deltas.get(contract.cost_frequency)
622 while (startdate < d) & (startdate < datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
624 'amount': contract.cost_generated,
625 'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
626 'vehicle_id': contract.vehicle_id.id,
627 'cost_subtype': contract.cost_subtype.id,
628 'contract_id': contract.id,
629 'auto_generated': True
631 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
632 startdate += deltas.get(contract.cost_frequency)
634 #for some reason when 2 scheduler run at the same time on the cron, we have a deadlock, so we only run it once and we call the other
636 self.pool.get('fleet.vehicle').run_scheduler(cr,uid,context=context)
640 def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
642 for record in self.browse(cr, uid, ids, context=context):
643 name = record.vehicle_id.name
644 if record.cost_subtype.name:
645 name += ' / '+ record.cost_subtype.name
647 name += ' / '+ record.date
648 res[record.id] = name
651 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
654 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
657 'odometer_unit': odometer_unit,
661 def compute_next_year_date(self, strdate):
662 oneyear = datetime.timedelta(days=365)
663 curdate = str_to_date(strdate)
664 return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
666 def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
668 return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
671 def get_days_left(self,cr,uid,ids,prop,unknow_none,context=None):
672 """return a dict with as value for each contract an integer
673 if contract is in an open state and is overdue, return 0
674 if contract is in a closed state, return -1
675 otherwise return the number of days before the contract expires
677 reads = self.browse(cr,uid,ids,context=context)
680 if (record.expiration_date and (record.state=='open' or record.state=='toclose')):
681 today= str_to_date(time.strftime('%Y-%m-%d'))
682 renew_date = str_to_date(record.expiration_date)
683 diff_time=int((renew_date-today).days)
687 res[record.id]=diff_time
692 def act_renew_contract(self,cr,uid,ids,context=None):
694 contracts = self.browse(cr,uid,ids,context=context)
695 for element in contracts:
696 default['date']=fields.date.context_today(self, cr, uid, context=context)
697 default['start_date']=str(str_to_date(element.expiration_date)+datetime.timedelta(days=1))
699 startdate = str_to_date(element.start_date)
700 enddate = str_to_date(element.expiration_date)
701 diffdate = (enddate-startdate)
702 newenddate = enddate+diffdate
703 default['expiration_date']=str(newenddate)
705 newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, ids[0], default, context=context)
706 mod,modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
708 'name':_("Renew Contract"),
711 'view_type': 'tree,form',
712 'res_model': 'fleet.vehicle.log.contract',
713 'type': 'ir.actions.act_window',
717 'context': {'active_id':newid},
720 def _get_default_contract_type(self, cr, uid, context=None):
722 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
727 def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
729 for element in cost_ids:
730 if element and len(element) == 3 and element[2] is not False:
731 totalsum += element[2].get('amount', 0.0)
734 'sum_cost': totalsum,
738 def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
740 for contract in self.browse(cr, uid, ids, context=context):
742 for cost in contract.cost_ids:
743 totalsum += cost.amount
744 res[contract.id] = totalsum
747 _inherits = {'fleet.vehicle.cost': 'cost_id'}
748 _name = 'fleet.vehicle.log.contract'
749 _description = 'Contract information on a vehicle'
750 _order='state desc,expiration_date'
752 'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
753 'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
754 'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
755 'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
756 'insurer_id' :fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
757 'purchaser_id': fields.many2one('res.partner', 'Contractor', domain="['|', ('customer','=',True), ('employee','=',True)]",help='Person to which the contract is signed for'),
758 'ins_ref': fields.char('Contract Reference', size=64),
759 'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')], 'Status', readonly=True, help='Choose wheter the contract is still valid or not'),
760 'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract'),
761 'cost_generated': fields.float('Recurring Cost Amount', help="Costs paid at regular intervals, depending on the cost frequency. If the cost frequency is set to unique, the cost will be logged at the start date"),
762 'cost_frequency': fields.selection([('no','No'), ('daily', 'Daily'), ('weekly','Weekly'), ('monthly','Monthly'), ('yearly','Yearly')], 'Recurring Cost Frequency', help='Frequency of the recuring cost', required=True),
763 'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs', ondelete='cascade'),
764 'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
765 'cost_amount': fields.related('cost_id', 'amount', string='Amount', type='float', store=True), #we need to keep this field as a related with store=True because the graph view doesn't support (1) to address fields from inherited table and (2) fields that aren't stored in database
768 'purchaser_id': lambda self, cr, uid, ctx: uid,
769 'date': fields.date.context_today,
770 'start_date': fields.date.context_today,
772 'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
773 'cost_frequency': 'no',
774 'cost_subtype': _get_default_contract_type,
775 'cost_type': 'contract',
778 def copy(self, cr, uid, id, default=None, context=None):
781 today = fields.date.context_today(self, cr, uid, context=context)
782 default['date'] = today
783 default['start_date'] = today
784 default['expiration_date'] = self.compute_next_year_date(today)
785 default['ins_ref'] = ''
786 default['state'] = 'open'
787 default['notes'] = ''
788 return super(fleet_vehicle_log_contract, self).copy(cr, uid, id, default, context=context)
790 def contract_close(self, cr, uid, ids, context=None):
791 return self.write(cr, uid, ids, {'state': 'closed'}, context=context)
793 def contract_open(self, cr, uid, ids, context=None):
794 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
796 class fleet_contract_state(osv.Model):
797 _name = 'fleet.contract.state'
798 _description = 'Contains the different possible status of a leasing contract'
801 'name':fields.char('Contract Status', size=32, required=True),