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 """
212 if context['xml_id']:
213 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet', context['xml_id'], context=context)
215 'default_vehicle_id': ids[0]
217 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
226 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'fleet','fleet_vehicle_costs_act', context)
228 'default_vehicle_id': ids[0],
229 'search_default_parent_false' : True
231 res['domain']=[('vehicle_id','=', ids[0])]
234 def _get_odometer(self, cr, uid, ids, odometer_id, arg, context):
235 res = dict.fromkeys(ids, 0)
236 for record in self.browse(cr,uid,ids,context=context):
237 ids = self.pool.get('fleet.vehicle.odometer').search(cr, uid, [('vehicle_id', '=', record.id)], limit=1, order='value desc')
239 res[record.id] = self.pool.get('fleet.vehicle.odometer').browse(cr, uid, ids[0], context=context).value
242 def _set_odometer(self, cr, uid, id, name, value, args=None, context=None):
244 date = fields.date.context_today(self, cr, uid, context=context)
245 data = {'value': value, 'date': date, 'vehicle_id': id}
246 return self.pool.get('fleet.vehicle.odometer').create(cr, uid, data, context=context)
248 def _search_get_overdue_contract_reminder(self, cr, uid, obj, name, args, context):
250 for field, operator, value in args:
251 assert operator == '>' and value == 0, 'Operation not supported'
252 today = fields.date.context_today(self, cr, uid, context=context)
253 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,))
254 res_ids = [x[0] for x in cr.fetchall()]
255 res.append(('id', 'in', res_ids))
258 def _search_contract_renewal_due_soon(self, cr, uid, obj, name, args, context):
260 for field, operator, value in args:
261 assert operator == '>' and value == 0, 'Operation not supported'
262 today = fields.date.context_today(self, cr, uid, context=context)
263 datetime_today = datetime.datetime.strptime(today, tools.DEFAULT_SERVER_DATE_FORMAT)
264 limit_date = str((datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
265 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))
266 res_ids = [x[0] for x in cr.fetchall()]
267 res.append(('id', 'in', res_ids))
270 def _get_contract_reminder_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
272 for record in self.browse(cr, uid, ids, context=context):
276 for element in record.log_contracts:
277 if (element.state in ('open', 'toclose') and element.expiration_date):
278 current_date_str = fields.date.context_today(self, cr, uid, context=context)
279 due_time_str = element.expiration_date
280 current_date = str_to_date(current_date_str)
281 due_time = str_to_date(due_time_str)
282 diff_time = (due_time-current_date).days
285 if diff_time<15 and diff_time>=0:
286 due_soon = due_soon +1;
287 if overdue+due_soon>0:
288 ids = self.pool.get('fleet.vehicle.log.contract').search(cr,uid,[('vehicle_id','=',record.id),'|',('state','=','open'),('state','=','toclose')],limit=1,order='expiration_date asc')
290 name=(self.pool.get('fleet.vehicle.log.contract').browse(cr,uid,ids[0],context=context).cost_subtype.name)
293 'contract_renewal_overdue':overdue,
294 'contract_renewal_due_soon':due_soon,
295 'contract_renewal_total':(overdue+due_soon-1),
296 'contract_renewal_name':name,
300 def run_scheduler(self, cr, uid, context=None):
301 datetime_today = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT)
302 limit_date = (datetime_today + relativedelta(days=+15)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
303 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)
305 for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, ids, context=context):
306 if contract.vehicle_id.id in res:
307 res[contract.vehicle_id.id] += 1
309 res[contract.vehicle_id.id] = 1
311 for vehicle, value in res.items():
312 self.message_post(cr, uid, vehicle, body=_('%s contract(s) need(s) to be renewed and/or closed!') % (str(value)), context=context)
314 return self.pool.get('fleet.vehicle.log.contract').write(cr, uid, ids, {'state': 'toclose'}, context=context)
316 def _get_default_state(self, cr, uid, context):
318 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'vehicle_state_active')
323 _name = 'fleet.vehicle'
324 _description = 'Information on a vehicle'
325 _order= 'license_plate asc'
327 'name': fields.function(_vehicle_name_get_fnc, type="char", string='Name', store=True),
328 'company_id': fields.many2one('res.company', 'Company'),
329 'license_plate': fields.char('License Plate', size=32, required=True, help='License plate number of the vehicle (ie: plate number for a car)'),
330 'vin_sn': fields.char('Chassis Number', size=32, help='Unique number written on the vehicle motor (VIN/SN number)'),
331 'driver': fields.many2one('res.partner', 'Driver', help='Driver of the vehicle'),
332 'model_id': fields.many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle'),
333 'log_fuel': fields.one2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs'),
334 'log_services': fields.one2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs'),
335 'log_contracts': fields.one2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts'),
336 'acquisition_date': fields.date('Acquisition Date', required=False, help='Date when the vehicle has been bought'),
337 'color': fields.char('Color', size=32, help='Color of the vehicle'),
338 'state': fields.many2one('fleet.vehicle.state', 'State', help='Current state of the vehicle', ondelete="set null"),
339 'location': fields.char('Location', size=128, help='Location of the vehicle (garage, ...)'),
340 'seats': fields.integer('Seats Number', help='Number of seats of the vehicle'),
341 'doors': fields.integer('Doors Number', help='Number of doors of the vehicle'),
342 'tag_ids' :fields.many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id','tag_id', 'Tags'),
343 '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'),
344 'odometer_unit': fields.selection([('kilometers', 'Kilometers'),('miles','Miles')], 'Odometer Unit', help='Unit of the odometer ',required=True),
345 'transmission': fields.selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle'),
346 'fuel_type': fields.selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle'),
347 'horsepower': fields.integer('Horsepower'),
348 'horsepower_tax': fields.float('Horsepower Taxation'),
349 'power': fields.integer('Power (kW)', help='Power in kW of the vehicle'),
350 'co2': fields.float('CO2 Emissions', help='CO2 emissions of the vehicle'),
351 'image': fields.related('model_id', 'image', type="binary", string="Logo"),
352 'image_medium': fields.related('model_id', 'image_medium', type="binary", string="Logo"),
353 'image_small': fields.related('model_id', 'image_small', type="binary", string="Logo"),
354 'contract_renewal_due_soon': fields.function(_get_contract_reminder_fnc, fnct_search=_search_contract_renewal_due_soon, type="integer", string='Contracts to renew', multi='contract_info'),
355 'contract_renewal_overdue': fields.function(_get_contract_reminder_fnc, fnct_search=_search_get_overdue_contract_reminder, type="integer", string='Contracts Overdued', multi='contract_info'),
356 'contract_renewal_name': fields.function(_get_contract_reminder_fnc, type="text", string='Name of contract to renew soon', multi='contract_info'),
357 'contract_renewal_total': fields.function(_get_contract_reminder_fnc, type="integer", string='Total of contracts due or overdue minus one', multi='contract_info'),
358 'car_value': fields.float('Car Value', help='Value of the bought vehicle'),
363 'odometer_unit': 'kilometers',
364 'state': _get_default_state,
367 def copy(self, cr, uid, id, default=None, context=None):
377 return super(fleet_vehicle, self).copy(cr, uid, id, default, context=context)
379 def on_change_model(self, cr, uid, ids, model_id, context=None):
382 model = self.pool.get('fleet.vehicle.model').browse(cr, uid, model_id, context=context)
385 'image_medium': model.image,
389 def create(self, cr, uid, data, context=None):
390 vehicle_id = super(fleet_vehicle, self).create(cr, uid, data, context=context)
391 vehicle = self.browse(cr, uid, vehicle_id, context=context)
392 self.message_post(cr, uid, [vehicle_id], body=_('Vehicle %s has been added to the fleet!') % (vehicle.license_plate), context=context)
395 def write(self, cr, uid, ids, vals, context=None):
397 This function write an entry in the openchatter whenever we change important information
398 on the vehicle like the model, the drive, the state of the vehicle or its license plate
400 for vehicle in self.browse(cr, uid, ids, context):
402 if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
403 value = self.pool.get('fleet.vehicle.model').browse(cr,uid,vals['model_id'],context=context).name
404 oldmodel = vehicle.model_id.name or _('None')
405 changes.append(_('Model: from \' %s \' to \' %s \'') %(oldmodel, value))
406 if 'driver' in vals and vehicle.driver.id != vals['driver']:
407 value = self.pool.get('res.partner').browse(cr,uid,vals['driver'],context=context).name
408 olddriver = (vehicle.driver.name) or _('None')
409 changes.append(_('Driver: from \' %s \' to \' %s \'') %(olddriver, value))
410 if 'state' in vals and vehicle.state.id != vals['state']:
411 value = self.pool.get('fleet.vehicle.state').browse(cr,uid,vals['state'],context=context).name
412 oldstate = vehicle.state.name or _('None')
413 changes.append(_('State: from \' %s \' to \' %s \'') %(oldstate, value))
414 if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
415 old_license_plate = vehicle.license_plate or _('None')
416 changes.append(_('License Plate: from \' %s \' to \' %s \'') %(old_license_plate, vals['license_plate']))
419 self.message_post(cr, uid, [vehicle.id], body=", ".join(changes), context=context)
421 vehicle_id = super(fleet_vehicle,self).write(cr, uid, ids, vals, context)
425 class fleet_vehicle_odometer(osv.Model):
426 _name='fleet.vehicle.odometer'
427 _description='Odometer log for a vehicle'
430 def _vehicle_log_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
432 for record in self.browse(cr, uid, ids, context=context):
433 name = record.vehicle_id.name
435 name = name+ ' / '+ str(record.date)
436 res[record.id] = name
439 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
442 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
445 'unit': odometer_unit,
450 'name': fields.function(_vehicle_log_name_get_fnc, type="char", string='Name', store=True),
451 'date': fields.date('Date'),
452 'value': fields.float('Odometer Value', group_operator="max"),
453 'vehicle_id': fields.many2one('fleet.vehicle', 'Vehicle', required=True),
454 'unit': fields.related('vehicle_id', 'odometer_unit', type="char", string="Unit", readonly=True),
457 'date': fields.date.context_today,
461 class fleet_vehicle_log_fuel(osv.Model):
463 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
466 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
469 'odometer_unit': odometer_unit,
473 def on_change_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
474 if liter > 0 and price_per_liter > 0:
475 return {'value' : {'amount' : liter * price_per_liter,}}
476 elif liter > 0 and amount > 0:
477 return {'value' : {'price_per_liter' : amount / liter,}}
478 elif price_per_liter > 0 and amount > 0:
479 return {'value' : {'liter' : amount / price_per_liter,}}
483 def on_change_price_per_liter(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
485 if price_per_liter > 0 and liter > 0:
486 return {'value' : {'amount' : liter * price_per_liter,}}
487 elif price_per_liter > 0 and amount > 0:
488 return {'value' : {'liter' : amount / price_per_liter,}}
489 elif liter > 0 and amount > 0:
490 return {'value' : {'price_per_liter' : amount / liter,}}
494 def on_change_amount(self, cr, uid, ids, liter, price_per_liter, amount, context=None):
496 if amount > 0 and liter > 0:
497 return {'value': {'price_per_liter': amount / liter}}
498 elif amount > 0 and price_per_liter > 0:
499 return {'value': {'liter': amount / price_per_liter}}
500 elif liter > 0 and price_per_liter > 0:
501 return {'value': {'amount': liter * price_per_liter}}
504 def _get_default_service_type(self, cr, uid, context):
506 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_refueling')
511 _name = 'fleet.vehicle.log.fuel'
512 _description = 'Fuel log for vehicles'
513 _inherits = {'fleet.vehicle.cost': 'cost_id'}
516 'liter': fields.float('Liter'),
517 'price_per_liter': fields.float('Price Per Liter'),
518 'purchaser_id': fields.many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]"),
519 'inv_ref': fields.char('Invoice Reference', size=64),
520 'vendor_id': fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
521 'notes': fields.text('Notes'),
522 '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
525 'purchaser_id': lambda self, cr, uid, ctx: uid,
526 'date': fields.date.context_today,
527 'cost_subtype': _get_default_service_type,
532 class fleet_vehicle_log_services(osv.Model):
534 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
537 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
540 'odometer_unit': odometer_unit,
544 def _get_default_service_type(self, cr, uid, context):
546 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_service_service_8')
551 _inherits = {'fleet.vehicle.cost': 'cost_id'}
552 _name = 'fleet.vehicle.log.services'
553 _description = 'Services for vehicles'
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 '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
559 'notes': fields.text('Notes'),
562 'purchaser_id': lambda self, cr, uid, ctx: uid,
563 'date': fields.date.context_today,
564 'cost_subtype': _get_default_service_type,
565 'cost_type': 'services'
569 class fleet_service_type(osv.Model):
570 _name = 'fleet.service.type'
571 _description = 'Type of services available on a vehicle'
573 'name': fields.char('Name', required=True, translate=True),
574 'category': fields.selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category', required=True, help='Choose wheter the service refer to contracts, vehicle services or both'),
578 class fleet_vehicle_log_contract(osv.Model):
580 def run_scheduler(self,cr,uid,context=None):
581 #This method is called by a cron task
582 #It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
583 #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
584 #If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
585 #The created costs are associated to a contract thanks to the many2one field contract_id
586 #If the contract has no start_date, no cost will be created, even if the contract has recurring costs
587 vehicle_cost_obj = self.pool.get('fleet.vehicle.cost')
588 d = datetime.datetime.strptime(fields.date.context_today(self, cr, uid, context=context), tools.DEFAULT_SERVER_DATE_FORMAT).date()
589 contract_ids = self.pool.get('fleet.vehicle.log.contract').search(cr, uid, [('state','!=','closed')], offset=0, limit=None, order=None,context=None, count=False)
590 deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
591 for contract in self.pool.get('fleet.vehicle.log.contract').browse(cr, uid, contract_ids, context=context):
592 if not contract.start_date or contract.cost_frequency == 'no':
595 last_cost_date = contract.start_date
596 if contract.generated_cost_ids:
597 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)
598 if last_autogenerated_cost_id:
600 last_cost_date = vehicle_cost_obj.browse(cr, uid, last_cost_id[0], context=context).date
601 startdate = datetime.datetime.strptime(last_cost_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()
603 startdate += deltas.get(contract.cost_frequency)
604 while (startdate < d) & (startdate < datetime.datetime.strptime(contract.expiration_date, tools.DEFAULT_SERVER_DATE_FORMAT).date()):
606 'amount': contract.cost_generated,
607 'date': startdate.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
608 'vehicle_id': contract.vehicle_id.id,
609 'cost_subtype': contract.cost_subtype.id,
610 'contract_id': contract.id,
611 'auto_generated': True
613 cost_id = self.pool.get('fleet.vehicle.cost').create(cr, uid, data, context=context)
614 startdate += deltas.get(contract.cost_frequency)
616 #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
618 self.pool.get('fleet.vehicle').run_scheduler(cr,uid,context=context)
622 def _vehicle_contract_name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
624 for record in self.browse(cr, uid, ids, context=context):
625 name = record.vehicle_id.name
626 if record.cost_subtype.name:
627 name += ' / '+ record.cost_subtype.name
629 name += ' / '+ record.date
630 res[record.id] = name
633 def on_change_vehicle(self, cr, uid, ids, vehicle_id, context=None):
636 odometer_unit = self.pool.get('fleet.vehicle').browse(cr, uid, vehicle_id, context=context).odometer_unit
639 'odometer_unit': odometer_unit,
643 def compute_next_year_date(self, strdate):
644 oneyear = datetime.timedelta(days=365)
645 curdate = str_to_date(strdate)
646 return datetime.datetime.strftime(curdate + oneyear, tools.DEFAULT_SERVER_DATE_FORMAT)
648 def on_change_start_date(self, cr, uid, ids, strdate, enddate, context=None):
650 return {'value': {'expiration_date': self.compute_next_year_date(strdate),}}
653 def get_days_left(self,cr,uid,ids,prop,unknow_none,context=None):
654 """return a dict with as value for each contract an integer
655 if contract is in an open state and is overdue, return 0
656 if contract is in a closed state, return -1
657 otherwise return the number of days before the contract expires
659 reads = self.browse(cr,uid,ids,context=context)
662 if (record.expiration_date and (record.state=='open' or record.state=='toclose')):
663 today= str_to_date(time.strftime('%Y-%m-%d'))
664 renew_date = str_to_date(record.expiration_date)
665 diff_time=int((renew_date-today).days)
669 res[record.id]=diff_time
674 def act_renew_contract(self,cr,uid,ids,context=None):
676 contracts = self.browse(cr,uid,ids,context=context)
677 for element in contracts:
678 default['date']=fields.date.context_today(self, cr, uid, context=context)
679 default['start_date']=str(str_to_date(element.expiration_date)+datetime.timedelta(days=1))
681 startdate = str_to_date(element.start_date)
682 enddate = str_to_date(element.expiration_date)
683 diffdate = (enddate-startdate)
684 newenddate = enddate+diffdate
685 default['expiration_date']=str(newenddate)
687 newid = super(fleet_vehicle_log_contract, self).copy(cr, uid, ids[0], default, context=context)
688 mod,modid = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'fleet_vehicle_log_contract_form')
690 'name':_("Renew Contract"),
693 'view_type': 'tree,form',
694 'res_model': 'fleet.vehicle.log.contract',
695 'type': 'ir.actions.act_window',
699 'context': {'active_id':newid},
702 def _get_default_contract_type(self, cr, uid, context=None):
704 model, model_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'fleet', 'type_contract_leasing')
709 def on_change_indic_cost(self, cr, uid, ids, cost_ids, context=None):
711 for element in cost_ids:
712 if element and len(element) == 3 and element[2] is not False:
713 totalsum += element[2].get('amount', 0.0)
716 'sum_cost': totalsum,
720 def _get_sum_cost(self, cr, uid, ids, field_name, arg, context=None):
722 for contract in self.browse(cr, uid, ids, context=context):
724 for cost in contract.cost_ids:
725 totalsum += cost.amount
726 res[contract.id] = totalsum
729 _inherits = {'fleet.vehicle.cost': 'cost_id'}
730 _name = 'fleet.vehicle.log.contract'
731 _description = 'Contract information on a vehicle'
732 _order='state desc,expiration_date'
734 'name': fields.function(_vehicle_contract_name_get_fnc, type="text", string='Name', store=True),
735 'start_date': fields.date('Contract Start Date', help='Date when the coverage of the contract begins'),
736 'expiration_date': fields.date('Contract Expiration Date', help='Date when the coverage of the contract expirates (by default, one year after begin date)'),
737 'days_left': fields.function(get_days_left, type='integer', string='Warning Date'),
738 'insurer_id' :fields.many2one('res.partner', 'Supplier', domain="[('supplier','=',True)]"),
739 'purchaser_id': fields.many2one('res.partner', 'Contractor', domain="['|', ('customer','=',True), ('employee','=',True)]",help='Person to which the contract is signed for'),
740 'ins_ref': fields.char('Contract Reference', size=64),
741 'state': fields.selection([('open', 'In Progress'), ('toclose','To Close'), ('closed', 'Terminated')], 'Status', readonly=True, help='Choose wheter the contract is still valid or not'),
742 'notes': fields.text('Terms and Conditions', help='Write here all supplementary informations relative to this contract'),
743 '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"),
744 '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),
745 'generated_cost_ids': fields.one2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs', ondelete='cascade'),
746 'sum_cost': fields.function(_get_sum_cost, type='float', string='Indicative Costs Total'),
747 '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
750 'purchaser_id': lambda self, cr, uid, ctx: uid,
751 'date': fields.date.context_today,
752 'start_date': fields.date.context_today,
754 'expiration_date': lambda self, cr, uid, ctx: self.compute_next_year_date(fields.date.context_today(self, cr, uid, context=ctx)),
755 'cost_frequency': 'no',
756 'cost_subtype': _get_default_contract_type,
757 'cost_type': 'contract',
760 def copy(self, cr, uid, id, default=None, context=None):
763 today = fields.date.context_today(self, cr, uid, context=context)
764 default['date'] = today
765 default['start_date'] = today
766 default['expiration_date'] = self.compute_next_year_date(today)
767 default['ins_ref'] = ''
768 default['state'] = 'open'
769 default['notes'] = ''
770 return super(fleet_vehicle_log_contract, self).copy(cr, uid, id, default, context=context)
772 def contract_close(self, cr, uid, ids, context=None):
773 return self.write(cr, uid, ids, {'state': 'closed'},context=context)
775 def contract_open(self, cr, uid, ids, context=None):
776 return self.write(cr, uid, ids, {'state': 'open'},context=context)
778 class fleet_contract_state(osv.Model):
779 _name = 'fleet.contract.state'
780 _description = 'Contains the different possible status of a leasing contract'
783 'name':fields.char('Contract Status', size=32, required=True),