diff --git a/addons/fleet/__manifest__.py b/addons/fleet/__manifest__.py
index 74a3eb16c0e..623f655e995 100644
--- a/addons/fleet/__manifest__.py
+++ b/addons/fleet/__manifest__.py
@@ -31,7 +31,9 @@ Main Features
'data': [
'security/fleet_security.xml',
'security/ir.model.access.csv',
- 'views/fleet_view.xml',
+ 'views/fleet_vehicle_model_views.xml',
+ 'views/fleet_vehicle_views.xml',
+ 'views/fleet_vehicle_cost_views.xml',
'views/fleet_board_view.xml',
'data/fleet_cars_data.xml',
'data/fleet_data.xml',
diff --git a/addons/fleet/models/__init__.py b/addons/fleet/models/__init__.py
index da96d13235c..9ed0370b3f4 100644
--- a/addons/fleet/models/__init__.py
+++ b/addons/fleet/models/__init__.py
@@ -1 +1,6 @@
-import fleet
\ No newline at end of file
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import fleet_vehicle
+import fleet_vehicle_cost
+import fleet_vehicle_model
diff --git a/addons/fleet/models/fleet.py b/addons/fleet/models/fleet.py
deleted file mode 100644
index 3dfd4538990..00000000000
--- a/addons/fleet/models/fleet.py
+++ /dev/null
@@ -1,658 +0,0 @@
-# -*- 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', default=10)
-
- _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()
diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py
new file mode 100644
index 00000000000..4a6d67bd2bf
--- /dev/null
+++ b/addons/fleet/models/fleet_vehicle.py
@@ -0,0 +1,312 @@
+# -*- 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, _
+
+
+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('Immatriculation Date', required=False, help='Date when the vehicle has been immatriculated')
+ 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(string="Catalog Value (VAT incl.)", help='Value of the bought vehicle')
+ residual_value = fields.Float()
+
+ _sql_constraints = [
+ ('driver_id_unique', 'UNIQUE(driver_id)', 'Only one car can be assigned to the same employee!')
+ ]
+
+ @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), ('state', '=', 'open')])
+ 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)
+ driver_id = fields.Many2one(related="vehicle_id.driver_id", string="Driver")
+
+ @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 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 FleetVehicleTag(models.Model):
+ _name = 'fleet.vehicle.tag'
+
+ name = fields.Char(required=True, translate=True)
+ color = fields.Integer('Color Index', default=10)
+
+ _sql_constraints = [('name_uniq', 'unique (name)', "Tag name already exists !")]
+
+
+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')
diff --git a/addons/fleet/models/fleet_vehicle_cost.py b/addons/fleet/models/fleet_vehicle_cost.py
new file mode 100644
index 00000000000..827863e3adc
--- /dev/null
+++ b/addons/fleet/models/fleet_vehicle_cost.py
@@ -0,0 +1,358 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+from dateutil.relativedelta import relativedelta
+
+
+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', copy=True)
+ 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)
+ description = fields.Char("Cost Description")
+
+ 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 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')
+ # 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
+ # (2) fields that aren't stored in database
+ cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=True)
+ odometer = fields.Float(string='Odometer at creation', help='Odometer measure of the vehicle at the moment of the contract creation')
+
+ @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()
+
+
+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')
+ # 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
+ # (2) fields that aren't stored in database
+ cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=True)
+
+ @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
diff --git a/addons/fleet/models/fleet_vehicle_model.py b/addons/fleet/models/fleet_vehicle_model.py
new file mode 100644
index 00000000000..eb1cef20120
--- /dev/null
+++ b/addons/fleet/models/fleet_vehicle_model.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, tools
+
+
+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)
diff --git a/addons/fleet/views/fleet_vehicle_cost_views.xml b/addons/fleet/views/fleet_vehicle_cost_views.xml
new file mode 100644
index 00000000000..51dceaf016f
--- /dev/null
+++ b/addons/fleet/views/fleet_vehicle_cost_views.xml
@@ -0,0 +1,512 @@
+
+
+
+
+ fleet.vehicle.cost.tree
+ fleet.vehicle.cost
+
+
+
+
+
+
+
+
+
+
+
+ fleet.vehicle.cost.pivot
+ fleet.vehicle.cost
+
+
+
+
+
+
+
+
+
+
+
+ fleet.vehicle.cost.graph
+ fleet.vehicle.cost
+
+
+
+
+
+
+
+
+
+
+ fleet.vehicle.cost.pivot
+ fleet.vehicle.cost
+
+
+
+
+
+
+
+
+
+
+ fleet.vehicle.cost.graph
+ fleet.vehicle.cost
+
+
+
+
+
+
+
+
+
+
+ fleet.vehicle.cost.search
+ fleet.vehicle.cost
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ fleet.vehicle.cost.form
+ fleet.vehicle.cost
+
+
+
+
+
+
+ fleet.vehicle.cost.graph
+ fleet.vehicle.cost
+
+
+
+
+
+
+
+
+
+
+ Vehicle Costs
+ fleet.vehicle.cost
+ form
+ tree,form,graph
+ {"search_default_parent_false" : True, "search_default_groupby_vehicle_id" : True,}
+
+
+ Click to create a new cost.
+
+ Odoo helps you managing the costs for your different
+ vehicles. Costs are created automatically from services,
+ contracts (fixed or recurring) and fuel logs.
+
+ Manage all your contracts (leasing, insurances, etc.) with
+ their related services, costs. Odoo will automatically warn
+ you when some contracts have to be renewed.
+
+ Each contract (e.g.: leasing) may include several services
+ (reparation, insurances, periodic maintenance).
+
+ Odoo helps you keeping track of all the services done
+ on your vehicle. Services can be of many type: occasional
+ repair, fixed maintenance, etc.
+
+ You will be able to manage your fleet by keeping track of the
+ contracts, services, fixed and recurring costs, odometers and
+ fuel logs associated to each vehicle.
+
+ Odoo will warn you when services or contract have to be
+ renewed.
+
- You will be able to manage your fleet by keeping track of the
- contracts, services, fixed and recurring costs, odometers and
- fuel logs associated to each vehicle.
-
- Odoo will warn you when services or contract have to be
- renewed.
-
- Manage all your contracts (leasing, insurances, etc.) with
- their related services, costs. Odoo will automatically warn
- you when some contracts have to be renewed.
-
- Each contract (e.g.: leasing) may include several services
- (reparation, insurances, periodic maintenance).
-
- Odoo helps you keeping track of all the services done
- on your vehicle. Services can be of many type: occasional
- repair, fixed maintenance, etc.
-
- Odoo helps you managing the costs for your different
- vehicles. Costs are created automatically from services,
- contracts (fixed or recurring) and fuel logs.
-
- A contribution register is a third party involved in the salary
- payment of the employees. It can be the social security, the
- estate or anyone that collect or inject money on payslips.
-
+ A contribution register is a third party involved in the salary
+ payment of the employees. It can be the social security, the
+ estate or anyone that collect or inject money on payslips.
+