[MOV] fleet: Split models and views into separate files

It's a PITA to have a huge files with all the models into it. We took this opportunity to stick to the guideline.
This commit is contained in:
Yannick Tivisse
2017-04-26 15:08:09 +02:00
parent 2e7cdc1c90
commit c63e869fb2
10 changed files with 1812 additions and 1735 deletions
+3 -1
View File
@@ -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',
+6 -1
View File
@@ -1 +1,6 @@
import fleet
# -*- 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
-668
View File
@@ -1,668 +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', 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 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('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)
# TODO: Split models in hr_payroll and fleet into several files (this is a real pain in the arse)
@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 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
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()
+312
View File
@@ -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')
+358
View File
@@ -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
@@ -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)
@@ -0,0 +1,512 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id='fleet_vehicle_cost_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.tree</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<tree string="Vehicle Costs">
<field name="vehicle_id"/>
<field name="description"/>
<field name="cost_subtype_id"/>
<field name="date"/>
</tree>
</field>
</record>
<record id='fleet_vehicle_cost_view_pivot' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.pivot</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<pivot string="Vehicles costs">
<field name="vehicle_id" type="row"/>
<field name="cost_type" type="row"/>
<field name="date" interval="year" type="col"/>
<field name="amount" type="measure"/>
</pivot>
</field>
</record>
<record id='fleet_vehicle_cost_view_graph' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.graph</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<graph string="Vehicles costs">
<field name="vehicle_id"/>
<field name="date" interval="year"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_cost_indicative_view_pivot' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.pivot</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<pivot string="Vehicles costs" >
<field name="vehicle_id" type="row"/>
<field name="cost_type" type="col"/>
<field name="amount" type="measure"/>
</pivot>
</field>
</record>
<record id='fleet_vehicle_cost_indicative_view_graph' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.graph</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<graph string="Vehicles costs">
<field name="parent_id" type="row"/>
<field name="cost_type" type="col"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_costs_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.search</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<search string="Vehicles costs" >
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="date"/>
<field name="parent_id"/>
<filter name="parent_false" domain="[('parent_id','=',False)]" string="Effective Costs"/>
<filter name="parent_true" domain="[('parent_id','!=',False)]" string="Indicative Costs"/>
<group expand="1" string="Group By">
<filter name="groupby_cost_type" context="{'group_by': 'cost_type'}" string="Cost Type"/>
<filter name="groupby_cost_subtype_id" context="{'group_by': 'cost_subtype_id'}" string="Cost Subtype"/>
<filter name="groupby_vehicle_id" context="{'group_by': 'vehicle_id'}" string="Vehicle"/>
<filter name="groupby_parent_id" context="{'group_by': 'parent_id'}" string="Parent"/>
<separator/>
<filter name="groupby_date" context="{'group_by': 'date:month'}" string="Month" help="Vehicle Costs by Month"/>
<filter name="groupby_year" context="{'group_by': 'date:year'}" string="Year"/>
</group>
</search>
</field>
</record>
<record id='fleet_vehicle_costs_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.cost.form</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<form string="Vehicle costs">
<sheet>
<group col="2" string="Cost Details">
<group>
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="amount"/>
<field name="description"/>
</group>
<group>
<field name="date"/>
<field name="parent_id" groups="base.group_no_one"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="fleet_vehicle_costs_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.cost.graph</field>
<field name="model">fleet.vehicle.cost</field>
<field name="arch" type="xml">
<graph string="Costs Per Month" stacked="True">
<field name="date" type="row"/>
<field name="vehicle_id" type="col"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_costs_action' model='ir.actions.act_window'>
<field name="name">Vehicle Costs</field>
<field name="res_model">fleet.vehicle.cost</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{"search_default_parent_false" : True, "search_default_groupby_vehicle_id" : True,}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new cost.
</p><p>
Odoo helps you managing the costs for your different
vehicles. Costs are created automatically from services,
contracts (fixed or recurring) and fuel logs.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_costs_action" parent="fleet_vehicles" id="fleet_vehicle_costs_menu" groups="fleet_group_manager"/>
<record id='fleet_vehicle_log_contract_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.log_contract.form</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<form string="Contract logs">
<header>
<button modifiers="{'invisible': [['state', '=', 'closed']]}" name="contract_close" states="open" type="object" class="oe_highlight" string="Terminate Contract" groups="fleet.fleet_group_manager"/>
<button modifiers="{'invisible': [['state', '=', 'closed']]}" name="contract_close" states="toclose" type="object" class="oe_highlight" string="Terminate Contract" groups="fleet.fleet_group_manager"/>
<button modifiers="{'invisible': [['state', 'not in', ['closed']]]}" name="contract_open" states="closed" type="object" class="oe_highlight" string="Set Contract In Progress" groups="fleet.fleet_group_manager"/>
<button class="oe_highlight" name="act_renew_contract" type="object" string="Renew Contract" help="Create a new contract automatically with all the same informations except for the date that will start at the end of current contract" groups="fleet.fleet_group_manager"/>
<field name="state" widget="statusbar" />
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<group col="2">
<group string="Contract details">
<field name="vehicle_id"/>
<field name="cost_subtype_id" required="1" domain="['|',('category','=','contract'),('category','=','both')]"/>
<field name="amount" string="Activation Cost" help="Cost that is paid only once at the creation of the contract"/>
<label for="cost_generated"/>
<div>
<field name="cost_generated" class="oe_inline" attrs="{'invisible': [('cost_frequency','=','no')]}" />
<field name="cost_frequency" class="oe_inline" />
</div>
</group>
<group string="Odometer details">
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
</group>
</group>
<group col="2">
<group>
<field name="date" string="Invoice Date"/>
<field name="start_date"/>
<field name="expiration_date" />
</group>
<group>
<field name="insurer_id" context="{'default_supplier': True}"/>
<field name="purchaser_id" />
<field name="ins_ref" />
</group>
</group>
<notebook>
<page string="Included Services">
<group>
<field name="cost_ids" context="{'vehicle_id': vehicle_id}" nolabel="1">
<tree editable="bottom">
<field name="cost_subtype_id" string="Service" domain="[('category','=','service')]"/>
<field name="description"/>
<field name="amount" sum="Price" string="Indicative Cost" />
</tree>
</field>
</group>
<div class="oe_right">
<group>
<field name="sum_cost" string="Indicative Costs Total"/>
</group>
</div>
</page>
<page string="Generated Recurring Costs">
<group>
<field name="generated_cost_ids" context="{'vehicle_id': vehicle_id}" nolabel="1" sum="amount">
<tree editable="bottom" >
<field name="date" />
<field name="amount" sum="amount"/>
</tree>
</field>
</group>
</page>
</notebook>
<group string="Terms and Conditions">
<field name="notes" nolabel="1" placeholder="Write here all other information relative to this contract" />
</group>
</sheet>
</form>
</field>
</record>
<act_window
id="act_renew_contract"
name="Renew Contract"
res_model="fleet.vehicle.log.contract"
src_model="fleet.vehicle.log.contract"
view_mode="form"
view_type="form"
/>
<record id='fleet_vehicle_log_contract_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.contract.tree</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<tree string="Contract logs" decoration-warning="days_left>0 and days_left&lt;15" decoration-danger="days_left==0" decoration-muted="state=='closed'">
<field name="active" invisible="1"/>
<field name="start_date" />
<field name="expiration_date" />
<field name="days_left" invisible="1"/>
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="insurer_id" />
<field name="amount" string="Activation Cost"/>
<field name="cost_generated"/>
<field name="cost_frequency"/>
<field name="state" />
</tree>
</field>
</record>
<record id="fleet_vehicle_log_contract_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.log.contract.graph</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<graph string="Contract Costs Per Month" type="bar" stacked="True">
<field name="date" type="row"/>
<field name="vehicle_id" type="row"/>
<field name="cost_amount" type="measure"/>
</graph>
</field>
</record>
<record id="fleet_vehicle_log_contract_view_search" model="ir.ui.view">
<field name="name">fleet.vehicle.log.contract.search</field>
<field name="model">fleet.vehicle.log.contract</field>
<field name="arch" type="xml">
<search string="Vehicles Contracts">
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<filter string="In Progress" name="open" domain="[('state', '=', 'open')]"/>
</search>
</field>
</record>
<record id='fleet_vehicle_log_contract_action' model='ir.actions.act_window'>
<field name="name">Vehicles Contracts</field>
<field name="res_model">fleet.vehicle.log.contract</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{'search_default_open': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
</p><p>
Manage all your contracts (leasing, insurances, etc.) with
their related services, costs. Odoo will automatically warn
you when some contracts have to be renewed.
</p><p>
Each contract (e.g.: leasing) may include several services
(reparation, insurances, periodic maintenance).
</p>
</field>
</record>
<menuitem action="fleet_vehicle_log_contract_action" parent="fleet_vehicles" id="fleet_vehicle_log_contract_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_log_fuel_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.fuel.form</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<form string="Fuel Logs">
<sheet>
<group>
<group string="Vehicle Details">
<field name="vehicle_id"/>
</group>
<group string="Refueling Details">
<field name="liter"/>
<field name="price_per_liter"/>
<field name="amount"/>
</group>
</group>
<group>
<group string="Odometer Details">
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
</group>
<group string="Additional Details">
<field name="date" />
<field name="purchaser_id" />
<field name="inv_ref" />
<field name="vendor_id" context="{'default_supplier': True}"/>
</group>
</group>
<field name="notes" placeholder="Write here any other information"/>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_log_fuel_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.fuel.tree</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<tree string="Fuel Logs">
<field name="date" />
<field name="vehicle_id" />
<field name="odometer" invisible="1"/>
<field name="odometer_unit" invisible="1"/>
<field name="purchaser_id" />
<field name="inv_ref" invisible="1"/>
<field name="vendor_id" invisible="1"/>
<field name="liter" />
<field name="price_per_liter" invisible="1"/>
<field name="amount" sum="Price"/>
</tree>
</field>
</record>
<record id='fleet_vehicle_log_fuel_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.fuel.search</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<search string="Vehicles Fuel Logs" >
<field name="vehicle_id" />
<field name="date"/>
<field name="purchaser_id" />
<field name="liter" />
<field name="amount" sum="Price"/>
<group expand="0" string="Group By">
<filter name="groupby_vehicle" context="{'group_by' : 'vehicle_id'}" string="Vehicle"/>
</group>
</search>
</field>
</record>
<record id="fleet_vehicle_log_fuel_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.log.fuel.graph</field>
<field name="model">fleet.vehicle.log.fuel</field>
<field name="arch" type="xml">
<graph string="Fuel Costs Per Month">
<field name="vehicle_id" type="row"/>
<field name="cost_amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_log_fuel_action' model='ir.actions.act_window'>
<field name="name">Vehicles Fuel Logs</field>
<field name="res_model">fleet.vehicle.log.fuel</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{"search_default_groupby_vehicle" : True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new fuel log.
</p><p>
Here you can add refuelling entries for all vehicles. You can
also filter logs of a particular vehicle using the search
field.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_log_fuel_action" parent="fleet_vehicles" id="fleet_vehicle_log_fuel_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_log_services_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.services.form</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<form string="Services Logs">
<sheet>
<group col="2">
<group string="Services Details">
<field name="vehicle_id"/>
<field name="cost_subtype_id" string="Service Type" domain="['|',('category','=','service'),('category','=','both')]" required="1"/>
<field name="amount"/>
</group>
<group string="Odometer Details">
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
</group>
</group>
<group col="2">
<group string="Additional Details">
<field name="date" />
<field name="purchaser_id" />
<field name="vendor_id" context="{'default_supplier': True}"/>
<field name="inv_ref" />
</group>
</group>
<group string="Included Services">
<field name="cost_ids" nolabel="1">
<tree string="Included Services" editable="bottom">
<field name="cost_subtype_id" string="Service" domain="[('category','=','service')]"/>
<field name="amount" sum="Price" string="Indicative Cost"/>
</tree>
</field>
</group>
<group string="Notes">
<field nolabel="1" name="notes" placeholder="Write here any other information related to the service completed."/>
</group>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_log_services_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.services.tree</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<tree string="Services Logs">
<field name="date" />
<field name="vehicle_id" />
<field name="cost_subtype_id"/>
<field name="purchaser_id"/>
<field name="vendor_id" />
<field name="inv_ref" />
<field name="notes" />
<field name="amount" sum="Total"/>
</tree>
</field>
</record>
<record id="fleet_vehicle_log_services_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.log.services.graph</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<graph string="Services Costs Per Month" stacked="True">
<field name="date" type="row"/>
<field name="vehicle_id" type="row"/>
<field name="cost_amount" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_log_services_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.log.services.search</field>
<field name="model">fleet.vehicle.log.services</field>
<field name="arch" type="xml">
<search string="Services Logs" >
<field name="vehicle_id"/>
<field name="cost_subtype_id"/>
</search>
</field>
</record>
<record id='fleet_vehicle_log_services_action' model='ir.actions.act_window'>
<field name="name">Vehicles Services Logs</field>
<field name="res_model">fleet.vehicle.log.services</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new service entry.
</p><p>
Odoo helps you keeping track of all the services done
on your vehicle. Services can be of many type: occasional
repair, fixed maintenance, etc.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_log_services_action" parent="fleet_vehicles" id="fleet_vehicle_log_services_menu" groups="fleet_group_user"/>
</odoo>
@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id='fleet_vehicle_model_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.form</field>
<field name="model">fleet.vehicle.model</field>
<field name="arch" type="xml">
<form string="Model">
<sheet>
<field name="image_medium" widget='image' class="oe_avatar"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Model S"/>
</h1>
<label for="brand_id" class="oe_edit_only"/>
<h2>
<field name="brand_id" placeholder="e.g. Tesla"/>
</h2>
</div>
<notebook>
<page string="Vendors" name="vendors">
<field name="vendors" widget="many2many_kanban">
<kanban quick_create="false" create="true">
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div style="position: relative">
<a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
<div class="oe_module_vignette">
<div class="oe_module_desc">
<field name="name"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_model_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.tree</field>
<field name="model">fleet.vehicle.model</field>
<field name="arch" type="xml">
<tree string="Models">
<field name="brand_id" />
<field name="name" />
</tree>
</field>
</record>
<record id='fleet_vehicle_model_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.search</field>
<field name="model">fleet.vehicle.model</field>
<field name="arch" type="xml">
<search string="Vehicles costs" >
<field name="brand_id" />
<group expand="1" string="Group By">
<filter name="groupby_brand" context="{'group_by' : 'brand_id'}" string="Make"/>
</group>
</search>
</field>
</record>
<record id='fleet_vehicle_model_action' model='ir.actions.act_window'>
<field name="name">Vehicle Model</field>
<field name="res_model">fleet.vehicle.model</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{"search_default_groupby_brand" : True,}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new model.
</p><p>
You can define several models (e.g. A3, A4) for each make (Audi).
</p>
</field>
</record>
<menuitem name="Fleet" id="menu_root" sequence="115" groups="fleet_group_user" web_icon="fleet,static/description/icon.png"/>
<menuitem name="Configuration" parent="menu_root" id="fleet_configuration" sequence="100" groups="fleet_group_manager"/>
<menuitem action="fleet_vehicle_model_action" parent="fleet_configuration" id="fleet_vehicle_model_menu"/>
<record id='fleet_vehicle_model_brand_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.brand.tree</field>
<field name="model">fleet.vehicle.model.brand</field>
<field name="arch" type="xml">
<tree string="Model Make">
<field name="name" />
</tree>
</field>
</record>
<record id='fleet_vehicle_model_brand_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.brand.form</field>
<field name="model">fleet.vehicle.model.brand</field>
<field name="arch" type="xml">
<form string="Model Make">
<group>
<div>
<field name="image_medium" widget="image" class="oe_avatar"/>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
</div>
</group>
</form>
</field>
</record>
<record id='fleet_vehicle_model_brand_view_kanban' model='ir.ui.view'>
<field name="name">fleet.vehicle.model.brandkanban</field>
<field name="model">fleet.vehicle.model.brand</field>
<field name="arch" type="xml">
<kanban>
<field name="id"/>
<field name="name" />
<field name="image" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_vignette oe_semantic_html_override">
<a type="open" href="#" class="oe_kanban_action">
<img t-att-src="kanban_image('fleet.vehicle.model.brand', 'image_small', record.id.value)" class="img-responsive" style="width:100px;"/>
</a>
<div class="text-center">
<h4 class="oe_partner_heading">
<a type="open">
<field name="name"/>
</a>
</h4>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id='fleet_vehicle_model_brand_action' model='ir.actions.act_window'>
<field name="name">Model make of Vehicle</field>
<field name="res_model">fleet.vehicle.model.brand</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new make.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_model_brand_action" parent="fleet_configuration" id="fleet_vehicle_model_brand_menu"/>
</odoo>
+400
View File
@@ -0,0 +1,400 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id='fleet_vehicle_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.form</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<form string="Vehicle">
<header>
<field name="state_id" widget="statusbar" clickable="True" />
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-book"
context="{'xml_id':'fleet_vehicle_log_contract_action', 'search_default_open': 1}"
help="show the contract for this vehicle">
<field name="contract_count" widget="statinfo" string="Contracts"/>
</button>
<button name="act_show_log_cost"
type="object"
class="oe_stat_button"
icon="fa-usd"
help="show all the costs for this vehicle"
groups="fleet.fleet_group_manager">
<field name="cost_count" widget="statinfo" string="Costs"/>
</button>
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-wrench"
context="{'xml_id':'fleet_vehicle_log_services_action'}"
help="show the services logs for this vehicle" >
<field name="service_count" widget="statinfo" string="Services"/>
</button>
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-flask"
context="{'xml_id':'fleet_vehicle_log_fuel_action'}"
help="show the fuel logs for this vehicle" >
<field name="fuel_logs_count" widget="statinfo" string="Fuel"/>
</button>
<button name="return_action_to_open"
type="object"
class="oe_stat_button"
icon="fa-tachometer"
context="{'xml_id':'fleet_vehicle_odometer_action'}"
help="show the odometer logs for this vehicle" >
<field name="odometer_count" widget="statinfo" string="Odometer"/>
</button>
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<field name="image_medium" widget='image' class="oe_avatar"/>
<div class="oe_title">
<label for="model_id" class="oe_edit_only"/>
<h1>
<field name="model_id" placeholder="e.g. Model S"/>
</h1>
<label for="license_plate" class="oe_edit_only"/>
<h2>
<field name="license_plate" class="oe_inline" placeholder="e.g. PAE 326"/>
</h2>
<label for="tag_ids" class="oe_edit_only"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
</div>
<group col="2" string="General Properties">
<group >
<field name="driver_id" />
<field name="location" />
<field name="vin_sn" />
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group >
<label for="odometer"/>
<div class="o_row">
<field name="odometer"/>
<field name="odometer_unit"/>
</div>
<field name="acquisition_date" />
<field name="car_value" />
<field name="residual_value"/>
</group>
</group>
<group col="2">
<group string="Additional Properties">
<field name="seats" />
<field name="doors" />
<field name="color" />
</group>
<group string="Engine Options">
<field name="transmission" />
<field name="fuel_type" />
<label for="co2"/>
<div class="o_row" name="co2">
<field name="co2"/><span>g/km</span>
</div>
<field name="horsepower" />
<field name="horsepower_tax" />
<label for="power"/>
<div class="o_row">
<field name="power"/><span>kW</span>
</div>
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread" options='{"thread_level": 1}'/>
</div>
</form>
</field>
</record>
<record id='fleet_vehicle_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.tree</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<tree string="Vehicle" decoration-warning="contract_renewal_due_soon and not contract_renewal_overdue" decoration-danger="contract_renewal_overdue">
<field name="active" invisible="1"/>
<field name="license_plate" />
<field name="model_id" />
<field name="driver_id" />
<field name="vin_sn" />
<field name="acquisition_date" />
<field name="state_id"/>
<field name="odometer"/>
<field name="odometer_unit" />
<field name="contract_renewal_due_soon" invisible="1"/>
<field name="contract_renewal_overdue" invisible="1" />
<field name="contract_renewal_total" invisible="1"/>
</tree>
</field>
</record>
<record id="fleet_vehicle_view_search" model="ir.ui.view">
<field name="name">fleet.vehicle.search</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<search string="All vehicles">
<field name="name" filter_domain="['|', ('name','ilike',self), ('license_plate','ilike',self)]" string="Vehicle"/>
<field name="driver_id"/>
<field name="tag_ids"/>
<field name="location"/>
<field name="state_id" />
<filter name="alert_true" domain="['|',('contract_renewal_due_soon','=',True),('contract_renewal_overdue','=',True)]" string="Has Alert(s)"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group expand="1" string="Group By">
<filter name="groupby_status" context="{'group_by' : 'state_id'}" string="Status"/>
<filter name="groupby_model" context="{'group_by' : 'model_id'}" string="Model"/>
</group>
</search>
</field>
</record>
<record id='fleet_vehicle_view_kanban' model='ir.ui.view'>
<field name="name">fleet.vehicle.kanban</field>
<field name="model">fleet.vehicle</field>
<field name="arch" type="xml">
<kanban>
<field name="license_plate" />
<field name="model_id" />
<field name="driver_id" />
<field name="location" />
<field name="state_id" />
<field name="id" />
<field name="image" />
<field name="tag_ids" />
<field name="contract_renewal_due_soon" />
<field name="contract_renewal_overdue" />
<field name="contract_renewal_name" />
<field name="contract_renewal_total" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_tags_section">
<field name="tag_ids"/>
</div>
<div class="o_kanban_image">
<img t-att-src="kanban_image('fleet.vehicle', 'image_small', record.id.value)"/>
</div>
<div class="oe_kanban_details">
<strong>
<field name="license_plate"/> : <field name="model_id"/>
</strong>
<ul>
<li>
<t t-if="record.driver_id.raw_value"><field name="driver_id"/></t>
</li>
<li>
<t t-if="record.location.raw_value"><field name="location"/></t>
</li>
</ul>
<div class="o_kanban_button">
<a t-if="record.contract_renewal_due_soon.raw_value and !record.contract_renewal_overdue.raw_value" data-type="object" data-name="return_action_to_open" href="#" class="oe_kanban_action btn btn-sm btn-warning" data-context='{"xml_id":"fleet_vehicle_log_contract_action"}'>
<field name="contract_renewal_name"/> <t t-if="record.contract_renewal_total.raw_value > 0"> and <field name="contract_renewal_total" /> other(s)</t>
</a>
<a t-if="record.contract_renewal_overdue.raw_value" data-type="object" data-name="return_action_to_open" href="#" class="oe_kanban_action btn btn-sm btn-danger" data-context='{"xml_id":"fleet_vehicle_log_contract_action"}'>
<field name="contract_renewal_name"/> <t t-if="record.contract_renewal_total.raw_value > 0"> and <field name="contract_renewal_total" /> other(s)</t>
</a>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id='fleet_vehicle_action' model='ir.actions.act_window'>
<field name="name">Vehicles</field>
<field name="res_model">fleet.vehicle</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new vehicle.
</p><p>
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.
</p><p>
Odoo will warn you when services or contract have to be
renewed.
</p>
</field>
</record>
<menuitem name="Vehicles" parent="menu_root" id="fleet_vehicles" sequence="2" groups="fleet_group_user"/>
<menuitem action="fleet_vehicle_action" parent="fleet_vehicles" id="fleet_vehicle_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_odometer_view_form' model='ir.ui.view'>
<field name="name">fleet.vehicle.odometer.form</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<form string="Odometer Logs">
<sheet>
<group>
<field name="vehicle_id"/>
<div>
<field name="value" class="oe_inline"/>
<field name="unit" class="oe_inline"/>
</div>
<field name="date" />
</group>
</sheet>
</form>
</field>
</record>
<record id='fleet_vehicle_odometer_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.odometer.tree</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<tree string="Odometer Logs" editable="top">
<field name="date" />
<field name="vehicle_id"/>
<field name="driver_id"/>
<field name="value" />
<field name="unit" />
</tree>
</field>
</record>
<record id='fleet_vehicle_odometer_view_search' model='ir.ui.view'>
<field name="name">fleet.vehicle.odometer.search</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<search string="Vehicles odometers" >
<field name="vehicle_id" />
<field name="value"/>
<field name="unit"/>
<field name="date"/>
<group expand="0" string="Group By">
<filter name="groupby_vehicle" context="{'group_by' : 'vehicle_id'}" string="Vehicle"/>
</group>
</search>
</field>
</record>
<record id="fleet_vehicle_odometer_view_graph" model="ir.ui.view">
<field name="name">fleet.vehicle.odometer.graph</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="arch" type="xml">
<graph string="Odometer Values Per Vehicle">
<field name="vehicle_id" type="row"/>
<field name="value" type="measure"/>
</graph>
</field>
</record>
<record id='fleet_vehicle_odometer_action' model='ir.actions.act_window'>
<field name="name">Vehicles Odometer</field>
<field name="res_model">fleet.vehicle.odometer</field>
<field name="view_type">form</field>
<field name="view_mode">tree,graph</field>
<field name="context">{"search_default_groupby_vehicle" : True}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new odometer log.
</p>
<p>
Here you can add various odometer entries for all vehicles.
You can also show odometer value for a particular vehicle using
the search field.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_odometer_action" parent="fleet_vehicles" id="fleet_vehicle_odometer_menu" groups="fleet_group_user"/>
<record id='fleet_vehicle_service_types_view_tree' model='ir.ui.view'>
<field name="name">fleet.service.type.tree</field>
<field name="model">fleet.service.type</field>
<field name="arch" type="xml">
<tree string="Service types" editable="top">
<field name="name" />
<field name="category"/>
</tree>
</field>
</record>
<record id='fleet_vehicle_service_types_action' model='ir.actions.act_window'>
<field name="name">Service Types</field>
<field name="res_model">fleet.service.type</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new type of service.
</p><p>
Each service can used in contracts, as a standalone service or both.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_service_types_action" parent="fleet_configuration" id="fleet_vehicle_service_types_menu"/>
<record id='fleet_vehicle_state_view_tree' model='ir.ui.view'>
<field name="name">fleet.vehicle.state.tree</field>
<field name="model">fleet.vehicle.state</field>
<field name="arch" type="xml">
<tree string="State" editable="bottom">
<field name="sequence" widget="handler"/>
<field name="name" />
</tree>
</field>
</record>
<record id='fleet_vehicle_state_action' model='ir.actions.act_window'>
<field name="name">Vehicle Status</field>
<field name="res_model">fleet.vehicle.state</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a vehicle status.
</p><p>
You can customize available status to track the evolution of
each vehicle. Example: Active, Being Repaired, Sold.
</p>
</field>
</record>
<menuitem action="fleet_vehicle_state_action" parent="fleet_configuration" id="fleet_vehicle_state_menu" groups="fleet_group_manager"/>
<record id="fleet_vehicle_tag_view_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.tag.form</field>
<field name="model">fleet.vehicle.tag</field>
<field name="arch" type="xml">
<form string="Vehicle Tags">
<group>
<field name="name"/>
</group>
</form>
</field>
</record>
<record id="fleet_vehicle_tag_action" model="ir.actions.act_window">
<field name="name">Vehicle Tags</field>
<field name="res_model">fleet.vehicle.tag</field>
<field name="view_type">form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a new tag.
</p>
</field>
</record>
<menuitem id="fleet_vehicle_tag_menu" parent="fleet_configuration" action="fleet_vehicle_tag_action" sequence="112"/>
</odoo>
File diff suppressed because it is too large Load Diff