Initial commit: Vertical Industry packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:52 +02:00
commit d5567a0017
766 changed files with 733028 additions and 0 deletions

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import fleet_service_type
from . import fleet_vehicle
from . import fleet_vehicle_assignation_log
from . import fleet_vehicle_log_contract
from . import fleet_vehicle_log_services
from . import fleet_vehicle_model
from . import fleet_vehicle_model_brand
from . import fleet_vehicle_model_category
from . import fleet_vehicle_odometer
from . import fleet_vehicle_state
from . import fleet_vehicle_tag
from . import res_config_settings
from . import res_partner

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class FleetServiceType(models.Model):
_name = 'fleet.service.type'
_description = 'Fleet Service Type'
_order = 'name'
name = fields.Char(required=True, translate=True)
category = fields.Selection([
('contract', 'Contract'),
('service', 'Service')
], 'Category', required=True, help='Choose whether the service refer to contracts, vehicle services or both')

View file

@ -0,0 +1,418 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.addons.fleet.models.fleet_vehicle_model import FUEL_TYPES
#Some fields don't have the exact same name
MODEL_FIELDS_TO_VEHICLE = {
'transmission': 'transmission', 'model_year': 'model_year', 'electric_assistance': 'electric_assistance',
'color': 'color', 'seats': 'seats', 'doors': 'doors', 'trailer_hook': 'trailer_hook',
'default_co2': 'co2', 'co2_standard': 'co2_standard', 'default_fuel_type': 'fuel_type',
'power': 'power', 'horsepower': 'horsepower', 'horsepower_tax': 'horsepower_tax', 'category_id': 'category_id',
}
class FleetVehicle(models.Model):
_inherit = ['mail.thread', 'mail.activity.mixin']
_name = 'fleet.vehicle'
_description = 'Vehicle'
_order = 'license_plate asc, acquisition_date asc'
_rec_names_search = ['name', 'driver_id.name']
def _get_default_state(self):
state = self.env.ref('fleet.fleet_vehicle_state_registered', raise_if_not_found=False)
return state if state and state.id else False
name = fields.Char(compute="_compute_vehicle_name", store=True)
description = fields.Html("Vehicle Description")
active = fields.Boolean('Active', default=True, tracking=True)
manager_id = fields.Many2one(
'res.users', 'Fleet Manager',
domain=lambda self: [('groups_id', 'in', self.env.ref('fleet.fleet_group_manager').id)],
)
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env.company,
)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
country_id = fields.Many2one('res.country', related='company_id.country_id')
country_code = fields.Char(related='country_id.code', depends=['country_id'])
license_plate = fields.Char(tracking=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)
trailer_hook = fields.Boolean(default=False, string='Trailer Hitch', compute='_compute_model_fields', store=True, readonly=False)
driver_id = fields.Many2one('res.partner', 'Driver', tracking=True, help='Driver address of the vehicle', copy=False)
future_driver_id = fields.Many2one('res.partner', 'Future Driver', tracking=True, help='Next Driver Address of the vehicle', copy=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
model_id = fields.Many2one('fleet.vehicle.model', 'Model',
tracking=True, required=True)
brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Brand', related="model_id.brand_id", store=True, readonly=False)
log_drivers = fields.One2many('fleet.vehicle.assignation.log', 'vehicle_id', string='Assignment Logs')
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts')
contract_count = fields.Integer(compute="_compute_count_all", string='Contract Count')
service_count = fields.Integer(compute="_compute_count_all", string='Services')
odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer')
history_count = fields.Integer(compute="_compute_count_all", string="Drivers History Count")
next_assignation_date = fields.Date('Assignment Date', help='This is the date at which the car will be available, if not set it means available instantly')
acquisition_date = fields.Date('Registration Date', required=False,
default=fields.Date.today, help='Date of vehicle registration')
write_off_date = fields.Date('Cancellation Date', tracking=True, help="Date when the vehicle's license plate has been cancelled/removed.")
first_contract_date = fields.Date(string="First Contract Date", default=fields.Date.today)
color = fields.Char(help='Color of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
state_id = fields.Many2one('fleet.vehicle.state', 'State',
default=_get_default_state, group_expand='_read_group_stage_ids',
tracking=True,
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', compute='_compute_model_fields', store=True, readonly=False)
model_year = fields.Char('Model Year', help='Year of the model', compute='_compute_model_fields', store=True, readonly=False)
doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
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', 'km'),
('miles', 'mi')
], 'Odometer Unit', default='kilometers', required=True)
transmission = fields.Selection(
[('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission',
compute='_compute_model_fields', store=True, readonly=False)
fuel_type = fields.Selection(FUEL_TYPES, 'Fuel Type', compute='_compute_model_fields', store=True, readonly=False)
horsepower = fields.Integer(compute='_compute_model_fields', store=True, readonly=False)
horsepower_tax = fields.Float('Horsepower Taxation', compute='_compute_model_fields', store=True, readonly=False)
power = fields.Integer('Power', help='Power in kW of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle', compute='_compute_model_fields', store=True, readonly=False, tracking=True)
co2_standard = fields.Char('CO2 Standard', compute='_compute_model_fields', store=True, readonly=False)
category_id = fields.Many2one('fleet.vehicle.model.category', 'Category', compute='_compute_model_fields', store=True, readonly=False)
image_128 = fields.Image(related='model_id.image_128', readonly=True)
contract_renewal_due_soon = fields.Boolean(compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon',
string='Has Contracts to renew')
contract_renewal_overdue = fields.Boolean(compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder',
string='Has Contracts Overdue')
contract_renewal_name = fields.Text(compute='_compute_contract_reminder', string='Name of contract to renew soon')
contract_renewal_total = fields.Text(compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one')
contract_state = fields.Selection(
[('futur', 'Incoming'),
('open', 'In Progress'),
('expired', 'Expired'),
('closed', 'Closed')
], string='Last Contract State', compute='_compute_contract_reminder', required=False)
car_value = fields.Float(string="Catalog Value (VAT Incl.)")
net_car_value = fields.Float(string="Purchase Value")
residual_value = fields.Float()
plan_to_change_car = fields.Boolean(related='driver_id.plan_to_change_car', store=True, readonly=False)
plan_to_change_bike = fields.Boolean(related='driver_id.plan_to_change_bike', store=True, readonly=False)
vehicle_type = fields.Selection(related='model_id.vehicle_type')
frame_type = fields.Selection([('diamant', 'Diamant'), ('trapez', 'Trapez'), ('wave', 'Wave')], string="Bike Frame Type")
electric_assistance = fields.Boolean(compute='_compute_model_fields', store=True, readonly=False)
frame_size = fields.Float()
service_activity = fields.Selection([
('none', 'None'),
('overdue', 'Overdue'),
('today', 'Today'),
], compute='_compute_service_activity')
@api.depends('log_services')
def _compute_service_activity(self):
for vehicle in self:
activities_state = set(state for state in vehicle.log_services.mapped('activity_state') if state and state != 'planned')
vehicle.service_activity = sorted(activities_state)[0] if activities_state else 'none'
@api.depends('model_id')
def _compute_model_fields(self):
'''
Copies all the related fields from the model to the vehicle
'''
model_values = dict()
for vehicle in self.filtered('model_id'):
if vehicle.model_id.id in model_values:
write_vals = model_values[vehicle.model_id.id]
else:
# copy if value is truthy
write_vals = {MODEL_FIELDS_TO_VEHICLE[key]: vehicle.model_id[key] for key in MODEL_FIELDS_TO_VEHICLE\
if vehicle.model_id[key]}
model_values[vehicle.model_id.id] = write_vals
vehicle.update(write_vals)
@api.depends('model_id.brand_id.name', 'model_id.name', 'license_plate')
def _compute_vehicle_name(self):
for record in self:
record.name = (record.model_id.brand_id.name or '') + '/' + (record.model_id.name or '') + '/' + (record.license_plate or _('No 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']
LogService = self.env['fleet.vehicle.log.services'].with_context(active_test=False)
LogContract = self.env['fleet.vehicle.log.contract'].with_context(active_test=False)
History = self.env['fleet.vehicle.assignation.log']
odometers_data = Odometer.read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id'], ['vehicle_id'])
services_data = LogService.read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id', 'active'], ['vehicle_id', 'active'], lazy=False)
logs_data = LogContract.read_group([('vehicle_id', 'in', self.ids), ('state', '!=', 'closed')], ['vehicle_id', 'active'], ['vehicle_id', 'active'], lazy=False)
histories_data = History.read_group([('vehicle_id', 'in', self.ids)], ['vehicle_id'], ['vehicle_id'])
mapped_odometer_data = defaultdict(lambda: 0)
mapped_service_data = defaultdict(lambda: defaultdict(lambda: 0))
mapped_log_data = defaultdict(lambda: defaultdict(lambda: 0))
mapped_history_data = defaultdict(lambda: 0)
for odometer_data in odometers_data:
mapped_odometer_data[odometer_data['vehicle_id'][0]] = odometer_data['vehicle_id_count']
for service_data in services_data:
mapped_service_data[service_data['vehicle_id'][0]][service_data['active']] = service_data['__count']
for log_data in logs_data:
mapped_log_data[log_data['vehicle_id'][0]][log_data['active']] = log_data['__count']
for history_data in histories_data:
mapped_history_data[history_data['vehicle_id'][0]] = history_data['vehicle_id_count']
for vehicle in self:
vehicle.odometer_count = mapped_odometer_data[vehicle.id]
vehicle.service_count = mapped_service_data[vehicle.id][vehicle.active]
vehicle.contract_count = mapped_log_data[vehicle.id][vehicle.active]
vehicle.history_count = mapped_history_data[vehicle.id]
@api.depends('log_contracts')
def _compute_contract_reminder(self):
params = self.env['ir.config_parameter'].sudo()
delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30))
for record in self:
overdue = False
due_soon = False
total = 0
name = ''
state = ''
for element in record.log_contracts:
if element.state in ('open', 'expired') 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 < delay_alert_contract:
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', 'expired'))
], limit=1, order='expiration_date asc')
if log_contract:
# we display only the name of the oldest overdue/due soon contract
name = log_contract.name
state = log_contract.state
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
record.contract_state = state
def _get_analytic_name(self):
# This function is used in fleet_account and is overrided in l10n_be_hr_payroll_fleet
return self.license_plate or _('No plate')
def _search_contract_renewal_due_soon(self, operator, value):
params = self.env['ir.config_parameter'].sudo()
delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30))
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=+delay_alert_contract))
res_ids = self.env['fleet.vehicle.log.contract'].search([
('expiration_date', '>', today),
('expiration_date', '<', limit_date),
('state', 'in', ['open', 'expired'])
]).mapped('vehicle_id').ids
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)
res_ids = self.env['fleet.vehicle.log.contract'].search([
('expiration_date', '!=', False),
('expiration_date', '<', today),
('state', 'in', ['open', 'expired'])
]).mapped('vehicle_id').ids
res.append(('id', search_operator, res_ids))
return res
def _clean_vals_internal_user(self, vals):
# Fleet administrator may not have rights to write on partner
# related fields when the driver_id is a res.user.
# This trick is used to prevent access right error.
su_vals = {}
if self.env.su:
return su_vals
if 'plan_to_change_car' in vals:
su_vals['plan_to_change_car'] = vals.pop('plan_to_change_car')
if 'plan_to_change_bike' in vals:
su_vals['plan_to_change_bike'] = vals.pop('plan_to_change_bike')
return su_vals
@api.model_create_multi
def create(self, vals_list):
ptc_values = [self._clean_vals_internal_user(vals) for vals in vals_list]
vehicles = super().create(vals_list)
for vehicle, vals, ptc_value in zip(vehicles, vals_list, ptc_values):
if ptc_value:
vehicle.sudo().write(ptc_value)
if 'driver_id' in vals and vals['driver_id']:
vehicle.create_driver_history(vals)
if 'future_driver_id' in vals and vals['future_driver_id']:
state_waiting_list = self.env.ref('fleet.fleet_vehicle_state_waiting_list', raise_if_not_found=False)
states = vehicle.mapped('state_id').ids
if not state_waiting_list or state_waiting_list.id not in states:
future_driver = self.env['res.partner'].browse(vals['future_driver_id'])
if self.vehicle_type == 'bike':
future_driver.sudo().write({'plan_to_change_bike': True})
if self.vehicle_type == 'car':
future_driver.sudo().write({'plan_to_change_car': True})
return vehicles
def write(self, vals):
if 'driver_id' in vals and vals['driver_id']:
driver_id = vals['driver_id']
for vehicle in self.filtered(lambda v: v.driver_id.id != driver_id):
vehicle.create_driver_history(vals)
if vehicle.driver_id:
vehicle.activity_schedule(
'mail.mail_activity_data_todo',
user_id=vehicle.manager_id.id or self.env.user.id,
note=_('Specify the End date of %s') % vehicle.driver_id.name)
if 'future_driver_id' in vals and vals['future_driver_id']:
state_waiting_list = self.env.ref('fleet.fleet_vehicle_state_waiting_list', raise_if_not_found=False)
states = self.mapped('state_id').ids if 'state_id' not in vals else [vals['state_id']]
if not state_waiting_list or state_waiting_list.id not in states:
future_driver = self.env['res.partner'].browse(vals['future_driver_id'])
if self.vehicle_type == 'bike':
future_driver.sudo().write({'plan_to_change_bike': True})
if self.vehicle_type == 'car':
future_driver.sudo().write({'plan_to_change_car': True})
if 'active' in vals and not vals['active']:
self.env['fleet.vehicle.log.contract'].search([('vehicle_id', 'in', self.ids)]).active = False
self.env['fleet.vehicle.log.services'].search([('vehicle_id', 'in', self.ids)]).active = False
su_vals = self._clean_vals_internal_user(vals)
if su_vals:
self.sudo().write(su_vals)
res = super(FleetVehicle, self).write(vals)
return res
def _get_driver_history_data(self, vals):
self.ensure_one()
return {
'vehicle_id': self.id,
'driver_id': vals['driver_id'],
'date_start': fields.Date.today(),
}
def create_driver_history(self, vals):
for vehicle in self:
self.env['fleet.vehicle.assignation.log'].create(
vehicle._get_driver_history_data(vals),
)
def action_accept_driver_change(self):
# Find all the vehicles of the same type for which the driver is the future_driver_id
# remove their driver_id and close their history using current date
vehicles = self.search([('driver_id', 'in', self.mapped('future_driver_id').ids), ('vehicle_type', '=', self.vehicle_type)])
vehicles.write({'driver_id': False})
for vehicle in self:
if vehicle.vehicle_type == 'bike':
vehicle.future_driver_id.sudo().write({'plan_to_change_bike': False})
if vehicle.vehicle_type == 'car':
vehicle.future_driver_id.sudo().write({'plan_to_change_car': False})
vehicle.driver_id = vehicle.future_driver_id
vehicle.future_driver_id = False
@api.model
def _read_group_stage_ids(self, stages, domain, order):
return self.env['fleet.vehicle.state'].search([], order=order)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
if 'co2' in fields:
fields.remove('co2')
return super(FleetVehicle, self).read_group(domain, fields, groupby, offset, limit, orderby, lazy)
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.%s' % 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
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()
copy_context = dict(self.env.context)
copy_context.pop('group_by', None)
res = self.env['ir.actions.act_window']._for_xml_id('fleet.fleet_vehicle_costs_action')
res.update(
context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
domain=[('vehicle_id', '=', self.id)]
)
return res
def _track_subtype(self, init_values):
self.ensure_one()
if 'driver_id' in init_values or 'future_driver_id' in init_values:
return self.env.ref('fleet.mt_fleet_driver_updated')
return super(FleetVehicle, self)._track_subtype(init_values)
def open_assignation_logs(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Assignment Logs',
'view_mode': 'tree',
'res_model': 'fleet.vehicle.assignation.log',
'domain': [('vehicle_id', '=', self.id)],
'context': {'default_driver_id': self.driver_id.id, 'default_vehicle_id': self.id}
}

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class FleetVehicleAssignationLog(models.Model):
_name = "fleet.vehicle.assignation.log"
_description = "Drivers history on a vehicle"
_order = "create_date desc, date_start desc"
vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True)
driver_id = fields.Many2one('res.partner', string="Driver", required=True)
date_start = fields.Date(string="Start Date")
date_end = fields.Date(string="End Date")

View file

@ -0,0 +1,151 @@
# -*- 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 FleetVehicleLogContract(models.Model):
_inherit = ['mail.thread', 'mail.activity.mixin']
_name = 'fleet.vehicle.log.contract'
_description = 'Vehicle Contract'
_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)
vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True, check_company=True)
cost_subtype_id = fields.Many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost', domain=[('category', '=', 'contract')])
amount = fields.Monetary('Cost', tracking=True)
date = fields.Date(help='Date when the cost has been executed')
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
name = fields.Char(string='Name', compute='_compute_contract_name', store=True, readonly=False)
active = fields.Boolean(default=True)
user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user, index=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')
expires_today = fields.Boolean(compute='_compute_days_left')
insurer_id = fields.Many2one('res.partner', 'Vendor')
purchaser_id = fields.Many2one(related='vehicle_id.driver_id', string='Driver')
ins_ref = fields.Char('Reference', size=64, copy=False)
state = fields.Selection(
[('futur', 'Incoming'),
('open', 'In Progress'),
('expired', 'Expired'),
('closed', 'Closed')
], 'Status', default='open', readonly=True,
help='Choose whether the contract is still valid or not',
tracking=True,
copy=False)
notes = fields.Html('Terms and Conditions', copy=False)
cost_generated = fields.Monetary('Recurring Cost', tracking=True)
cost_frequency = fields.Selection([
('no', 'No'),
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly')
], 'Recurring Cost Frequency', default='monthly', required=True)
service_ids = fields.Many2many('fleet.service.type', string="Included Services")
@api.depends('vehicle_id.name', 'cost_subtype_id')
def _compute_contract_name(self):
for record in self:
name = record.vehicle_id.name
if name and record.cost_subtype_id.name:
name = record.cost_subtype_id.name + ' ' + name
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
"""
today = fields.Date.from_string(fields.Date.today())
for record in self:
if record.expiration_date and record.state in ['open', 'expired']:
renew_date = fields.Date.from_string(record.expiration_date)
diff_time = (renew_date - today).days
record.days_left = diff_time if diff_time > 0 else 0
record.expires_today = diff_time == 0
else:
record.days_left = -1
record.expires_today = False
def write(self, vals):
res = super(FleetVehicleLogContract, self).write(vals)
if 'start_date' in vals or 'expiration_date' in vals:
date_today = fields.Date.today()
future_contracts, running_contracts, expired_contracts = self.env[self._name], self.env[self._name], self.env[self._name]
for contract in self.filtered(lambda c: c.start_date and c.state != 'closed'):
if date_today < contract.start_date:
future_contracts |= contract
elif not contract.expiration_date or contract.start_date <= date_today <= contract.expiration_date:
running_contracts |= contract
else:
expired_contracts |= contract
future_contracts.action_draft()
running_contracts.action_open()
expired_contracts.action_expire()
if vals.get('expiration_date') or vals.get('user_id'):
self.activity_reschedule(['fleet.mail_act_fleet_contract_to_renew'], date_deadline=vals.get('expiration_date'), new_user_id=vals.get('user_id'))
return res
def action_close(self):
self.write({'state': 'closed'})
def action_draft(self):
self.write({'state': 'futur'})
def action_open(self):
self.write({'state': 'open'})
def action_expire(self):
self.write({'state': 'expired'})
@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
params = self.env['ir.config_parameter'].sudo()
delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30))
date_today = fields.Date.from_string(fields.Date.today())
outdated_days = fields.Date.to_string(date_today + relativedelta(days=+delay_alert_contract))
reminder_activity_type = self.env.ref('fleet.mail_act_fleet_contract_to_renew', raise_if_not_found=False) or self.env['mail.activity.type']
nearly_expired_contracts = self.search([
('state', '=', 'open'),
('expiration_date', '<', outdated_days),
('user_id', '!=', False)
]
).filtered(
lambda nec: reminder_activity_type not in nec.activity_ids.activity_type_id
)
for contract in nearly_expired_contracts:
contract.activity_schedule(
'fleet.mail_act_fleet_contract_to_renew', contract.expiration_date,
user_id=contract.user_id.id)
expired_contracts = self.search([('state', 'not in', ['expired', 'closed']), ('expiration_date', '<',fields.Date.today() )])
expired_contracts.write({'state': 'expired'})
futur_contracts = self.search([('state', 'not in', ['futur', 'closed']), ('start_date', '>', fields.Date.today())])
futur_contracts.write({'state': 'futur'})
now_running_contracts = self.search([('state', '=', 'futur'), ('start_date', '<=', fields.Date.today())])
now_running_contracts.write({'state': 'open'})
def run_scheduler(self):
self.scheduler_manage_contract_expiration()

View file

@ -0,0 +1,75 @@
# -*- 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
class FleetVehicleLogServices(models.Model):
_name = 'fleet.vehicle.log.services'
_inherit = ['mail.thread', 'mail.activity.mixin']
_rec_name = 'service_type_id'
_description = 'Services for vehicles'
active = fields.Boolean(default=True)
vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True)
manager_id = fields.Many2one('res.users', 'Fleet Manager', related='vehicle_id.manager_id', store=True)
amount = fields.Monetary('Cost')
description = fields.Char('Description')
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', default=fields.Date.context_today)
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
purchaser_id = fields.Many2one('res.partner', string="Driver", compute='_compute_purchaser_id', readonly=False, store=True)
inv_ref = fields.Char('Vendor Reference')
vendor_id = fields.Many2one('res.partner', 'Vendor')
notes = fields.Text()
service_type_id = fields.Many2one(
'fleet.service.type', 'Service Type', required=True,
default=lambda self: self.env.ref('fleet.type_service_service_7', raise_if_not_found=False),
)
state = fields.Selection([
('new', 'New'),
('running', 'Running'),
('done', 'Done'),
('cancelled', 'Cancelled'),
], default='new', string='Stage', group_expand='_expand_states')
def _get_odometer(self):
self.odometer = 0
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_create_multi
def create(self, vals_list):
for data in vals_list:
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(FleetVehicleLogServices, self).create(vals_list)
@api.depends('vehicle_id')
def _compute_purchaser_id(self):
for service in self:
service.purchaser_id = service.vehicle_id.driver_id
def _expand_states(self, states, domain, order):
return [key for key, dummy in self._fields['state'].selection]

View file

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
FUEL_TYPES = [
('diesel', 'Diesel'),
('gasoline', 'Gasoline'),
('full_hybrid', 'Full Hybrid'),
('plug_in_hybrid_diesel', 'Plug-in Hybrid Diesel'),
('plug_in_hybrid_gasoline', 'Plug-in Hybrid Gasoline'),
('cng', 'CNG'),
('lpg', 'LPG'),
('hydrogen', 'Hydrogen'),
('electric', 'Electric'),
]
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', 'Manufacturer', required=True)
category_id = fields.Many2one('fleet.vehicle.model.category', 'Category')
vendors = fields.Many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors')
image_128 = fields.Image(related='brand_id.image_128', readonly=True)
active = fields.Boolean(default=True)
vehicle_type = fields.Selection([('car', 'Car'), ('bike', 'Bike')], default='car', required=True)
transmission = fields.Selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission')
vehicle_count = fields.Integer(compute='_compute_vehicle_count')
model_year = fields.Integer()
color = fields.Char()
seats = fields.Integer(string='Seats Number')
doors = fields.Integer(string='Doors Number')
trailer_hook = fields.Boolean(default=False, string='Trailer Hitch')
default_co2 = fields.Float('CO2 Emissions')
co2_standard = fields.Char()
default_fuel_type = fields.Selection(FUEL_TYPES, 'Fuel Type', default='electric')
power = fields.Integer('Power')
horsepower = fields.Integer()
horsepower_tax = fields.Float('Horsepower Taxation')
electric_assistance = fields.Boolean(default=False)
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
def _compute_vehicle_count(self):
group = self.env['fleet.vehicle']._read_group(
[('model_id', 'in', self.ids)], ['id', 'model_id'], groupby='model_id', lazy=False,
)
count_by_model = {entry['model_id'][0]: entry['__count'] for entry in group}
for model in self:
model.vehicle_count = count_by_model.get(model.id, 0)
def action_model_vehicle(self):
self.ensure_one()
view = {
'type': 'ir.actions.act_window',
'view_mode': 'kanban,tree,form',
'res_model': 'fleet.vehicle',
'name': _('Vehicles'),
'context': {'search_default_model_id': self.id, 'default_model_id': self.id}
}
return view

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class FleetVehicleModelBrand(models.Model):
_name = 'fleet.vehicle.model.brand'
_description = 'Brand of the vehicle'
_order = 'name asc'
name = fields.Char('Name', required=True)
image_128 = fields.Image("Logo", max_width=128, max_height=128)
model_count = fields.Integer(compute="_compute_model_count", string="", store=True)
model_ids = fields.One2many('fleet.vehicle.model', 'brand_id')
@api.depends('model_ids')
def _compute_model_count(self):
model_data = self.env['fleet.vehicle.model']._read_group([
('brand_id', 'in', self.ids),
], ['brand_id'], ['brand_id'])
models_brand = {x['brand_id'][0]: x['brand_id_count'] for x in model_data}
for record in self:
record.model_count = models_brand.get(record.id, 0)
def action_brand_model(self):
self.ensure_one()
view = {
'type': 'ir.actions.act_window',
'view_mode': 'tree,form',
'res_model': 'fleet.vehicle.model',
'name': 'Models',
'context': {'search_default_brand_id': self.id, 'default_brand_id': self.id}
}
return view

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class FleetVehicleModelCategory(models.Model):
_name = 'fleet.vehicle.model.category'
_description = 'Category of the model'
_order = 'sequence asc, id asc'
_sql_constraints = [
('name_uniq', 'UNIQUE (name)', 'Category name must be unique')
]
name = fields.Char(required=True)
sequence = fields.Integer()

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
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", readonly=False)
@api.depends('vehicle_id', 'date')
def _compute_vehicle_log_name(self):
for record in self:
name = record.vehicle_id.name
if not name:
name = str(record.date)
elif record.date:
name += ' / ' + str(record.date)
record.name = name
@api.onchange('vehicle_id')
def _onchange_vehicle(self):
if self.vehicle_id:
self.unit = self.vehicle_id.odometer_unit

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class FleetVehicleState(models.Model):
_name = 'fleet.vehicle.state'
_order = 'sequence asc'
_description = 'Vehicle Status'
name = fields.Char(required=True, translate=True)
sequence = fields.Integer()
_sql_constraints = [('fleet_state_name_unique', 'unique(name)', 'State name already exists')]

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class FleetVehicleTag(models.Model):
_name = 'fleet.vehicle.tag'
_description = 'Vehicle Tag'
name = fields.Char('Tag Name', required=True, translate=True)
color = fields.Integer('Color')
_sql_constraints = [('name_uniq', 'unique (name)', "Tag name already exists!")]

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = ['res.config.settings']
delay_alert_contract = fields.Integer(string='Delay alert contract outdated', default=30, config_parameter='hr_fleet.delay_alert_contract')

View file

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
plan_to_change_car = fields.Boolean('Plan To Change Car', default=False)
plan_to_change_bike = fields.Boolean('Plan To Change Bike', default=False)