659 lines
34 KiB
Python
659 lines
34 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models, tools, _
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class FleetVehicleCost(models.Model):
|
|
_name = 'fleet.vehicle.cost'
|
|
_description = 'Cost related to a vehicle'
|
|
_order = 'date desc, vehicle_id asc'
|
|
|
|
name = fields.Char(related='vehicle_id.name', string='Name', store=True)
|
|
vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True, help='Vehicle concerned by this log')
|
|
cost_subtype_id = fields.Many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost')
|
|
amount = fields.Float('Total Price')
|
|
cost_type = fields.Selection([('contract', 'Contract'), ('services', 'Services'), ('fuel', 'Fuel'), ('other', 'Other')],
|
|
'Category of the cost', default="other", help='For internal purpose only', required=True)
|
|
parent_id = fields.Many2one('fleet.vehicle.cost', 'Parent', help='Parent cost to this current cost')
|
|
cost_ids = fields.One2many('fleet.vehicle.cost', 'parent_id', 'Included Services')
|
|
odometer_id = fields.Many2one('fleet.vehicle.odometer', 'Odometer', help='Odometer measure of the vehicle at the moment of this log')
|
|
odometer = fields.Float(compute="_get_odometer", inverse='_set_odometer', string='Odometer Value', help='Odometer measure of the vehicle at the moment of this log')
|
|
odometer_unit = fields.Selection(related='vehicle_id.odometer_unit', string="Unit", readonly=True)
|
|
date = fields.Date(help='Date when the cost has been executed')
|
|
contract_id = fields.Many2one('fleet.vehicle.log.contract', 'Contract', help='Contract attached to this cost')
|
|
auto_generated = fields.Boolean('Automatically Generated', readonly=True)
|
|
|
|
def _get_odometer(self):
|
|
for record in self:
|
|
if record.odometer_id:
|
|
record.odometer = record.odometer_id.value
|
|
|
|
def _set_odometer(self):
|
|
for record in self:
|
|
if not record.odometer:
|
|
raise UserError(_('Emptying the odometer value of a vehicle is not allowed.'))
|
|
odometer = self.env['fleet.vehicle.odometer'].create({
|
|
'value': record.odometer,
|
|
'date': record.date or fields.Date.context_today(record),
|
|
'vehicle_id': record.vehicle_id.id
|
|
})
|
|
self.odometer_id = odometer
|
|
|
|
@api.model
|
|
def create(self, data):
|
|
#make sure that the data are consistent with values of parent and contract records given
|
|
if 'parent_id' in data and data['parent_id']:
|
|
parent = self.browse(data['parent_id'])
|
|
data['vehicle_id'] = parent.vehicle_id.id
|
|
data['date'] = parent.date
|
|
data['cost_type'] = parent.cost_type
|
|
if 'contract_id' in data and data['contract_id']:
|
|
contract = self.env['fleet.vehicle.log.contract'].browse(data['contract_id'])
|
|
data['vehicle_id'] = contract.vehicle_id.id
|
|
data['cost_subtype_id'] = contract.cost_subtype_id.id
|
|
data['cost_type'] = contract.cost_type
|
|
if 'odometer' in data and not data['odometer']:
|
|
#if received value for odometer is 0, then remove it from the data as it would result to the creation of a
|
|
#odometer log with 0, which is to be avoided
|
|
del(data['odometer'])
|
|
return super(FleetVehicleCost, self).create(data)
|
|
|
|
class FleetVehicleTag(models.Model):
|
|
_name = 'fleet.vehicle.tag'
|
|
|
|
name = fields.Char(required=True, translate=True)
|
|
color = fields.Integer('Color Index')
|
|
|
|
_sql_constraints = [('name_uniq', 'unique (name)', "Tag name already exists !")]
|
|
|
|
class FleetVehicleState(models.Model):
|
|
_name = 'fleet.vehicle.state'
|
|
_order = 'sequence asc'
|
|
|
|
name = fields.Char(required=True)
|
|
sequence = fields.Integer(help="Used to order the note stages")
|
|
|
|
_sql_constraints = [('fleet_state_name_unique', 'unique(name)', 'State name already exists')]
|
|
|
|
class FleetVehicleModel(models.Model):
|
|
_name = 'fleet.vehicle.model'
|
|
_description = 'Model of a vehicle'
|
|
_order = 'name asc'
|
|
|
|
name = fields.Char('Model name', required=True)
|
|
brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Make', required=True, help='Make of the vehicle')
|
|
vendors = fields.Many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors')
|
|
image = fields.Binary(related='brand_id.image', string="Logo")
|
|
image_medium = fields.Binary(related='brand_id.image_medium', string="Logo (medium)")
|
|
image_small = fields.Binary(related='brand_id.image_small', string="Logo (small)")
|
|
|
|
@api.multi
|
|
@api.depends('name', 'brand_id')
|
|
def name_get(self):
|
|
res = []
|
|
for record in self:
|
|
name = record.name
|
|
if record.brand_id.name:
|
|
name = record.brand_id.name + '/' + name
|
|
res.append((record.id, name))
|
|
return res
|
|
|
|
@api.onchange('brand_id')
|
|
def _onchange_brand(self):
|
|
if self.brand_id:
|
|
self.image_medium = self.brand_id.image
|
|
else:
|
|
self.image_medium = False
|
|
|
|
class FleetVehicleModelBrand(models.Model):
|
|
_name = 'fleet.vehicle.model.brand'
|
|
_description = 'Brand model of the vehicle'
|
|
_order = 'name asc'
|
|
|
|
name = fields.Char('Make', required=True)
|
|
image = fields.Binary("Logo", attachment=True,
|
|
help="This field holds the image used as logo for the brand, limited to 1024x1024px.")
|
|
image_medium = fields.Binary("Medium-sized image", attachment=True,
|
|
help="Medium-sized logo of the brand. It is automatically "
|
|
"resized as a 128x128px image, with aspect ratio preserved. "
|
|
"Use this field in form views or some kanban views.")
|
|
image_small = fields.Binary("Small-sized image", attachment=True,
|
|
help="Small-sized logo of the brand. It is automatically "
|
|
"resized as a 64x64px image, with aspect ratio preserved. "
|
|
"Use this field anywhere a small image is required.")
|
|
|
|
@api.model
|
|
def create(self, vals):
|
|
tools.image_resize_images(vals)
|
|
return super(FleetVehicleModelBrand, self).create(vals)
|
|
|
|
@api.multi
|
|
def write(self, vals):
|
|
tools.image_resize_images(vals)
|
|
return super(FleetVehicleModelBrand, self).write(vals)
|
|
|
|
class FleetVehicle(models.Model):
|
|
_inherit = 'mail.thread'
|
|
_name = 'fleet.vehicle'
|
|
_description = 'Information on a vehicle'
|
|
_order = 'license_plate asc'
|
|
|
|
def _get_default_state(self):
|
|
state = self.env.ref('fleet.vehicle_state_active', raise_if_not_found=False)
|
|
return state and state.id or False
|
|
|
|
name = fields.Char(compute="_compute_vehicle_name", store=True)
|
|
active = fields.Boolean(default=True)
|
|
company_id = fields.Many2one('res.company', 'Company')
|
|
license_plate = fields.Char(required=True, help='License plate number of the vehicle (i = plate number for a car)')
|
|
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
|
|
driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle')
|
|
model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle')
|
|
log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs')
|
|
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
|
|
log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts')
|
|
cost_count = fields.Integer(compute="_compute_count_all", string="Costs")
|
|
contract_count = fields.Integer(compute="_compute_count_all", string='Contracts')
|
|
service_count = fields.Integer(compute="_compute_count_all", string='Services')
|
|
fuel_logs_count = fields.Integer(compute="_compute_count_all", string='Fuel Logs')
|
|
odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer')
|
|
acquisition_date = fields.Date('Acquisition Date', required=False, help='Date when the vehicle has been bought')
|
|
color = fields.Char(help='Color of the vehicle')
|
|
state_id = fields.Many2one('fleet.vehicle.state', 'State', default=_get_default_state, help='Current state of the vehicle', ondelete="set null")
|
|
location = fields.Char(help='Location of the vehicle (garage, ...)')
|
|
seats = fields.Integer('Seats Number', help='Number of seats of the vehicle')
|
|
doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', default=5)
|
|
tag_ids = fields.Many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id', 'tag_id', 'Tags', copy=False)
|
|
odometer = fields.Float(compute='_get_odometer', inverse='_set_odometer', string='Last Odometer', help='Odometer measure of the vehicle at the moment of this log')
|
|
odometer_unit = fields.Selection([('kilometers', 'Kilometers'), ('miles', 'Miles')],
|
|
'Odometer Unit', default='kilometers', help='Unit of the odometer ', required=True)
|
|
transmission = fields.Selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle')
|
|
fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
|
|
horsepower = fields.Integer()
|
|
horsepower_tax = fields.Float('Horsepower Taxation')
|
|
power = fields.Integer('Power', help='Power in kW of the vehicle')
|
|
co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle')
|
|
image = fields.Binary(related='model_id.image', string="Logo")
|
|
image_medium = fields.Binary(related='model_id.image_medium', string="Logo (medium)")
|
|
image_small = fields.Binary(related='model_id.image_small', string="Logo (small)")
|
|
contract_renewal_due_soon = fields.Boolean(compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon', string='Has Contracts to renew', multi='contract_info')
|
|
contract_renewal_overdue = fields.Boolean(compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder', string='Has Contracts Overdue', multi='contract_info')
|
|
contract_renewal_name = fields.Text(compute='_compute_contract_reminder', string='Name of contract to renew soon', multi='contract_info')
|
|
contract_renewal_total = fields.Text(compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one', multi='contract_info')
|
|
car_value = fields.Float(help='Value of the bought vehicle')
|
|
|
|
@api.depends('model_id', 'license_plate')
|
|
def _compute_vehicle_name(self):
|
|
for record in self:
|
|
record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + record.license_plate
|
|
|
|
def _get_odometer(self):
|
|
FleetVehicalOdometer = self.env['fleet.vehicle.odometer']
|
|
for record in self:
|
|
vehicle_odometer = FleetVehicalOdometer.search([('vehicle_id', '=', record.id)], limit=1, order='value desc')
|
|
if vehicle_odometer:
|
|
record.odometer = vehicle_odometer.value
|
|
else:
|
|
record.odometer = 0
|
|
|
|
def _set_odometer(self):
|
|
for record in self:
|
|
if record.odometer:
|
|
date = fields.Date.context_today(record)
|
|
data = {'value': record.odometer, 'date': date, 'vehicle_id': record.id}
|
|
self.env['fleet.vehicle.odometer'].create(data)
|
|
|
|
def _compute_count_all(self):
|
|
Odometer = self.env['fleet.vehicle.odometer']
|
|
LogFuel = self.env['fleet.vehicle.log.fuel']
|
|
LogService = self.env['fleet.vehicle.log.services']
|
|
LogContract = self.env['fleet.vehicle.log.contract']
|
|
Cost = self.env['fleet.vehicle.cost']
|
|
for record in self:
|
|
record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)])
|
|
record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', record.id)])
|
|
record.service_count = LogService.search_count([('vehicle_id', '=', record.id)])
|
|
record.contract_count = LogContract.search_count([('vehicle_id', '=', record.id)])
|
|
record.cost_count = Cost.search_count([('vehicle_id', '=', record.id), ('parent_id', '=', False)])
|
|
|
|
@api.depends('log_contracts')
|
|
def _compute_contract_reminder(self):
|
|
for record in self:
|
|
overdue = False
|
|
due_soon = False
|
|
total = 0
|
|
name = ''
|
|
for element in record.log_contracts:
|
|
if element.state in ('open', 'toclose') and element.expiration_date:
|
|
current_date_str = fields.Date.context_today(record)
|
|
due_time_str = element.expiration_date
|
|
current_date = fields.Date.from_string(current_date_str)
|
|
due_time = fields.Date.from_string(due_time_str)
|
|
diff_time = (due_time - current_date).days
|
|
if diff_time < 0:
|
|
overdue = True
|
|
total += 1
|
|
if diff_time < 15 and diff_time >= 0:
|
|
due_soon = True
|
|
total += 1
|
|
if overdue or due_soon:
|
|
log_contract = self.env['fleet.vehicle.log.contract'].search([('vehicle_id', '=', record.id), ('state', 'in', ('open', 'toclose'))],
|
|
limit=1, order='expiration_date asc')
|
|
if log_contract:
|
|
#we display only the name of the oldest overdue/due soon contract
|
|
name = log_contract.cost_subtype_id.name
|
|
|
|
record.contract_renewal_overdue = overdue
|
|
record.contract_renewal_due_soon = due_soon
|
|
record.contract_renewal_total = total - 1 # we remove 1 from the real total for display purposes
|
|
record.contract_renewal_name = name
|
|
|
|
def _search_contract_renewal_due_soon(self, operator, value):
|
|
res = []
|
|
assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
|
|
if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
|
|
search_operator = 'in'
|
|
else:
|
|
search_operator = 'not in'
|
|
today = fields.Date.context_today(self)
|
|
datetime_today = fields.Datetime.from_string(today)
|
|
limit_date = fields.Datetime.to_string(datetime_today + relativedelta(days=+15))
|
|
self.env.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))
|
|
res_ids = [x[0] for x in self.env.cr.fetchall()]
|
|
res.append(('id', search_operator, res_ids))
|
|
return res
|
|
|
|
def _search_get_overdue_contract_reminder(self, operator, value):
|
|
res = []
|
|
assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
|
|
if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
|
|
search_operator = 'in'
|
|
else:
|
|
search_operator = 'not in'
|
|
today = fields.Date.context_today(self)
|
|
self.env.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,))
|
|
res_ids = [x[0] for x in self.env.cr.fetchall()]
|
|
res.append(('id', search_operator, res_ids))
|
|
return res
|
|
|
|
@api.onchange('model_id')
|
|
def _onchange_model(self):
|
|
if self.model_id:
|
|
self.image_medium = self.model_id.image
|
|
else:
|
|
self.image_medium = False
|
|
|
|
@api.model
|
|
def create(self, data):
|
|
vehicle = super(FleetVehicle, self.with_context(mail_create_nolog=True)).create(data)
|
|
vehicle.message_post(body=_('%s %s has been added to the fleet!') % (vehicle.model_id.name, vehicle.license_plate))
|
|
return vehicle
|
|
|
|
@api.multi
|
|
def write(self, vals):
|
|
"""
|
|
This function write an entry in the openchatter whenever we change important information
|
|
on the vehicle like the model, the drive, the state of the vehicle or its license plate
|
|
"""
|
|
for vehicle in self:
|
|
changes = []
|
|
if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
|
|
value = self.env['fleet.vehicle.model'].browse(vals['model_id']).name
|
|
oldmodel = vehicle.model_id.name or _('None')
|
|
changes.append(_("Model: from '%s' to '%s'") % (oldmodel, value))
|
|
if 'driver_id' in vals and vehicle.driver_id.id != vals['driver_id']:
|
|
value = self.env['res.partner'].browse(vals['driver_id']).name
|
|
olddriver = (vehicle.driver_id.name) or _('None')
|
|
changes.append(_("Driver: from '%s' to '%s'") % (olddriver, value))
|
|
if 'state_id' in vals and vehicle.state_id.id != vals['state_id']:
|
|
value = self.env['fleet.vehicle.state'].browse(vals['state_id']).name
|
|
oldstate = vehicle.state_id.name or _('None')
|
|
changes.append(_("State: from '%s' to '%s'") % (oldstate, value))
|
|
if 'license_plate' in vals and vehicle.license_plate != vals['license_plate']:
|
|
old_license_plate = vehicle.license_plate or _('None')
|
|
changes.append(_("License Plate: from '%s' to '%s'") % (old_license_plate, vals['license_plate']))
|
|
|
|
if len(changes) > 0:
|
|
self.message_post(body=", ".join(changes))
|
|
|
|
return super(FleetVehicle, self).write(vals)
|
|
|
|
@api.multi
|
|
def return_action_to_open(self):
|
|
""" This opens the xml view specified in xml_id for the current vehicle """
|
|
self.ensure_one()
|
|
xml_id = self.env.context.get('xml_id')
|
|
if xml_id:
|
|
res = self.env['ir.actions.act_window'].for_xml_id('fleet', xml_id)
|
|
res.update(
|
|
context=dict(self.env.context, default_vehicle_id=self.id, group_by=False),
|
|
domain=[('vehicle_id', '=', self.id)]
|
|
)
|
|
return res
|
|
return False
|
|
|
|
@api.multi
|
|
def act_show_log_cost(self):
|
|
""" This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
|
|
@return: the costs log view
|
|
"""
|
|
self.ensure_one()
|
|
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
|
|
res.update(
|
|
context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True),
|
|
domain=[('vehicle_id', '=', self.id)]
|
|
)
|
|
return res
|
|
|
|
class FleetVehicleOdometer(models.Model):
|
|
_name = 'fleet.vehicle.odometer'
|
|
_description = 'Odometer log for a vehicle'
|
|
_order = 'date desc'
|
|
|
|
name = fields.Char(compute='_compute_vehicle_log_name', store=True)
|
|
date = fields.Date(default=fields.Date.context_today)
|
|
value = fields.Float('Odometer Value', group_operator="max")
|
|
vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True)
|
|
unit = fields.Selection(related='vehicle_id.odometer_unit', string="Unit", readonly=True)
|
|
|
|
@api.depends('vehicle_id', 'date')
|
|
def _compute_vehicle_log_name(self):
|
|
for record in self:
|
|
name = record.vehicle_id.name
|
|
if not name:
|
|
name = record.date
|
|
elif record.date:
|
|
name += ' / ' + record.date
|
|
self.name = name
|
|
|
|
@api.onchange('vehicle_id')
|
|
def _onchange_vehicle(self):
|
|
if self.vehicle_id:
|
|
self.unit = self.vehicle_id.odometer_unit
|
|
|
|
class FleetVehicleLogFuel(models.Model):
|
|
_name = 'fleet.vehicle.log.fuel'
|
|
_description = 'Fuel log for vehicles'
|
|
_inherits = {'fleet.vehicle.cost': 'cost_id'}
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
res = super(FleetVehicleLogFuel, self).default_get(default_fields)
|
|
service = self.env.ref('fleet.type_service_refueling', raise_if_not_found=False)
|
|
res.update({
|
|
'date': fields.Date.context_today(self),
|
|
'cost_subtype_id': service and service.id or False,
|
|
'cost_type': 'fuel'
|
|
})
|
|
return res
|
|
|
|
liter = fields.Float()
|
|
price_per_liter = fields.Float()
|
|
purchaser_id = fields.Many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]")
|
|
inv_ref = fields.Char('Invoice Reference', size=64)
|
|
vendor_id = fields.Many2one('res.partner', 'Vendor', domain="[('supplier','=',True)]")
|
|
notes = fields.Text()
|
|
cost_id = fields.Many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade')
|
|
cost_amount = fields.Float(related='cost_id.amount', string='Amount', 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
|
|
|
|
@api.onchange('vehicle_id')
|
|
def _onchange_vehicle(self):
|
|
if self.vehicle_id:
|
|
self.odometer_unit = self.vehicle_id.odometer_unit
|
|
self.purchaser_id = self.vehicle_id.driver_id.id
|
|
|
|
@api.onchange('liter', 'price_per_liter', 'amount')
|
|
def _onchange_liter_price_amount(self):
|
|
#need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
|
|
#make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
|
|
#liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
|
|
#of 3.0/2=1.5)
|
|
#If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
|
|
#onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
|
|
#computation to 2 decimal
|
|
liter = float(self.liter)
|
|
price_per_liter = float(self.price_per_liter)
|
|
amount = float(self.amount)
|
|
if liter > 0 and price_per_liter > 0 and round(liter * price_per_liter, 2) != amount:
|
|
self.amount = round(liter * price_per_liter, 2)
|
|
elif amount > 0 and liter > 0 and round(amount / liter, 2) != price_per_liter:
|
|
self.price_per_liter = round(amount / liter, 2)
|
|
elif amount > 0 and price_per_liter > 0 and round(amount / price_per_liter, 2) != liter:
|
|
self.liter = round(amount / price_per_liter, 2)
|
|
|
|
class FleetVehicleLogServices(models.Model):
|
|
_name = 'fleet.vehicle.log.services'
|
|
_inherits = {'fleet.vehicle.cost': 'cost_id'}
|
|
_description = 'Services for vehicles'
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
res = super(FleetVehicleLogServices, self).default_get(default_fields)
|
|
service = self.env.ref('fleet.type_service_service_8', raise_if_not_found=False)
|
|
res.update({
|
|
'date': fields.Date.context_today(self),
|
|
'cost_subtype_id': service and service.id or False,
|
|
'cost_type': 'services'
|
|
})
|
|
return res
|
|
|
|
purchaser_id = fields.Many2one('res.partner', 'Purchaser', domain="['|',('customer','=',True),('employee','=',True)]")
|
|
inv_ref = fields.Char('Invoice Reference')
|
|
vendor_id = fields.Many2one('res.partner', 'Vendor', domain="[('supplier','=',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
|
|
cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=True)
|
|
notes = fields.Text()
|
|
cost_id = fields.Many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade')
|
|
|
|
@api.onchange('vehicle_id')
|
|
def _onchange_vehicle(self):
|
|
if self.vehicle_id:
|
|
self.odometer_unit = self.vehicle_id.odometer_unit
|
|
self.purchaser_id = self.vehicle_id.driver_id.id
|
|
|
|
class FleetServiceType(models.Model):
|
|
_name = 'fleet.service.type'
|
|
_description = 'Type of services available on a vehicle'
|
|
|
|
name = fields.Char(required=True, translate=True)
|
|
category = fields.Selection([('contract', 'Contract'), ('service', 'Service'), ('both', 'Both')], 'Category',
|
|
required=True, help='Choose wheter the service refer to contracts, vehicle services or both')
|
|
|
|
class FleetVehicleLogContract(models.Model):
|
|
|
|
_inherits = {'fleet.vehicle.cost': 'cost_id'}
|
|
_name = 'fleet.vehicle.log.contract'
|
|
_description = 'Contract information on a vehicle'
|
|
_order = 'state desc,expiration_date'
|
|
|
|
def compute_next_year_date(self, strdate):
|
|
oneyear = relativedelta(years=1)
|
|
start_date = fields.Date.from_string(strdate)
|
|
return fields.Date.to_string(start_date + oneyear)
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
res = super(FleetVehicleLogContract, self).default_get(default_fields)
|
|
contract = self.env.ref('fleet.type_contract_leasing', raise_if_not_found=False)
|
|
res.update({
|
|
'date': fields.Date.context_today(self),
|
|
'cost_subtype_id': contract and contract.id or False,
|
|
'cost_type': 'contract'
|
|
})
|
|
return res
|
|
|
|
name = fields.Text(compute='_compute_contract_name', store=True)
|
|
active = fields.Boolean(default=True)
|
|
start_date = fields.Date('Contract Start Date', default=fields.Date.context_today, help='Date when the coverage of the contract begins')
|
|
expiration_date = fields.Date('Contract Expiration Date', default=lambda self: self.compute_next_year_date(fields.Date.context_today(self)),
|
|
help='Date when the coverage of the contract expirates (by default, one year after begin date)')
|
|
days_left = fields.Integer(compute='_compute_days_left', string='Warning Date')
|
|
insurer_id = fields.Many2one('res.partner', 'Vendor')
|
|
purchaser_id = fields.Many2one('res.partner', 'Contractor', default=lambda self: self.env.user.partner_id.id, help='Person to which the contract is signed for')
|
|
ins_ref = fields.Char('Contract Reference', size=64, copy=False)
|
|
state = fields.Selection([('open', 'In Progress'), ('toclose', 'To Close'), ('closed', 'Terminated')],
|
|
'Status', default='open', readonly=True, help='Choose wheter the contract is still valid or not',
|
|
copy=False)
|
|
notes = fields.Text('Terms and Conditions', help='Write here all supplementary information relative to this contract', copy=False)
|
|
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")
|
|
cost_frequency = fields.Selection([('no', 'No'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('yearly', 'Yearly')], 'Recurring Cost Frequency',
|
|
default='no', help='Frequency of the recuring cost', required=True)
|
|
generated_cost_ids = fields.One2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs')
|
|
sum_cost = fields.Float(compute='_compute_sum_cost', string='Indicative Costs Total')
|
|
cost_id = fields.Many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade')
|
|
cost_amount = fields.Float(related='cost_id.amount', string='Amount', 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
|
|
|
|
@api.depends('vehicle_id', 'cost_subtype_id', 'date')
|
|
def _compute_contract_name(self):
|
|
for record in self:
|
|
name = record.vehicle_id.name
|
|
if record.cost_subtype_id.name:
|
|
name += ' / ' + record.cost_subtype_id.name
|
|
if record.date:
|
|
name += ' / ' + record.date
|
|
record.name = name
|
|
|
|
@api.depends('expiration_date', 'state')
|
|
def _compute_days_left(self):
|
|
"""return a dict with as value for each contract an integer
|
|
if contract is in an open state and is overdue, return 0
|
|
if contract is in a closed state, return -1
|
|
otherwise return the number of days before the contract expires
|
|
"""
|
|
for record in self:
|
|
if (record.expiration_date and (record.state == 'open' or record.state == 'toclose')):
|
|
today = fields.Date.from_string(fields.Date.today())
|
|
renew_date = fields.Date.from_string(record.expiration_date)
|
|
diff_time = (renew_date - today).days
|
|
record.days_left = diff_time > 0 and diff_time or 0
|
|
else:
|
|
record.days_left = -1
|
|
|
|
@api.depends('cost_ids.amount')
|
|
def _compute_sum_cost(self):
|
|
for contract in self:
|
|
contract.sum_cost = sum(contract.cost_ids.mapped('amount'))
|
|
|
|
@api.onchange('vehicle_id')
|
|
def _onchange_vehicle(self):
|
|
if self.vehicle_id:
|
|
self.odometer_unit = self.vehicle_id.odometer_unit
|
|
|
|
@api.multi
|
|
def contract_close(self):
|
|
for record in self:
|
|
record.state = 'closed'
|
|
|
|
@api.multi
|
|
def contract_open(self):
|
|
for record in self:
|
|
record.state = 'open'
|
|
|
|
@api.multi
|
|
def act_renew_contract(self):
|
|
assert len(self.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"
|
|
for element in self:
|
|
#compute end date
|
|
startdate = fields.Date.from_string(element.start_date)
|
|
enddate = fields.Date.from_string(element.expiration_date)
|
|
diffdate = (enddate - startdate)
|
|
default = {
|
|
'date': fields.Date.context_today(self),
|
|
'start_date': fields.Date.to_string(fields.Date.from_string(element.expiration_date) + relativedelta(days=1)),
|
|
'expiration_date': fields.Date.to_string(enddate + diffdate),
|
|
}
|
|
newid = element.copy(default).id
|
|
return {
|
|
'name': _("Renew Contract"),
|
|
'view_mode': 'form',
|
|
'view_id': self.env.ref('fleet.fleet_vehicle_log_contract_view_form').id,
|
|
'view_type': 'tree,form',
|
|
'res_model': 'fleet.vehicle.log.contract',
|
|
'type': 'ir.actions.act_window',
|
|
'domain': '[]',
|
|
'res_id': newid,
|
|
'context': {'active_id': newid},
|
|
}
|
|
|
|
@api.model
|
|
def scheduler_manage_auto_costs(self):
|
|
#This method is called by a cron task
|
|
#It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
|
|
#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
|
|
#If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
|
|
#The created costs are associated to a contract thanks to the many2one field contract_id
|
|
#If the contract has no start_date, no cost will be created, even if the contract has recurring costs
|
|
VehicleCost = self.env['fleet.vehicle.cost']
|
|
deltas = {'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1)}
|
|
contracts = self.env['fleet.vehicle.log.contract'].search([('state', '!=', 'closed')], offset=0, limit=None, order=None)
|
|
for contract in contracts:
|
|
if not contract.start_date or contract.cost_frequency == 'no':
|
|
continue
|
|
found = False
|
|
last_cost_date = contract.start_date
|
|
if contract.generated_cost_ids:
|
|
last_autogenerated_cost = VehicleCost.search([('contract_id', '=', contract.id), ('auto_generated', '=', True)], offset=0, limit=1, order='date desc')
|
|
if last_autogenerated_cost:
|
|
found = True
|
|
last_cost_date = last_autogenerated_cost.date
|
|
startdate = fields.Date.from_string(last_cost_date)
|
|
if found:
|
|
startdate += deltas.get(contract.cost_frequency)
|
|
today = fields.Date.from_string(fields.Date.context_today(self))
|
|
while (startdate <= today) & (startdate <= fields.Date.from_string(contract.expiration_date)):
|
|
data = {
|
|
'amount': contract.cost_generated,
|
|
'date': fields.Date.context_today(self),
|
|
'vehicle_id': contract.vehicle_id.id,
|
|
'cost_subtype_id': contract.cost_subtype_id.id,
|
|
'contract_id': contract.id,
|
|
'auto_generated': True
|
|
}
|
|
self.env['fleet.vehicle.cost'].create(data)
|
|
startdate += deltas.get(contract.cost_frequency)
|
|
return True
|
|
|
|
@api.model
|
|
def scheduler_manage_contract_expiration(self):
|
|
#This method is called by a cron task
|
|
#It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
|
|
date_today = fields.Date.from_string(fields.Date.context_today(self))
|
|
limit_date = fields.Date.to_string(date_today + relativedelta(days=+15))
|
|
contracts = self.search([('state', '=', 'open'), ('expiration_date', '<', limit_date)])
|
|
res = {}
|
|
for contract in contracts:
|
|
if contract.vehicle_id.id in res:
|
|
res[contract.vehicle_id.id] += 1
|
|
else:
|
|
res[contract.vehicle_id.id] = 1
|
|
|
|
Vehicle = self.env['fleet.vehicle']
|
|
for vehicle, value in res.items():
|
|
Vehicle.browse(vehicle).message_post(body=_('%s contract(s) need(s) to be renewed and/or closed!') % value)
|
|
return contracts.write({'state': 'toclose'})
|
|
|
|
@api.model
|
|
def run_scheduler(self):
|
|
self.scheduler_manage_auto_costs()
|
|
self.scheduler_manage_contract_expiration()
|