summaryrefslogtreecommitdiff
path: root/addons/fleet/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/fleet/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/fleet/models')
-rw-r--r--addons/fleet/models/__init__.py8
-rw-r--r--addons/fleet/models/fleet_vehicle.py397
-rw-r--r--addons/fleet/models/fleet_vehicle_cost.py189
-rw-r--r--addons/fleet/models/fleet_vehicle_model.py66
-rw-r--r--addons/fleet/models/res_config_settings.py11
-rw-r--r--addons/fleet/models/res_partner.py10
6 files changed, 681 insertions, 0 deletions
diff --git a/addons/fleet/models/__init__.py b/addons/fleet/models/__init__.py
new file mode 100644
index 00000000..d0515378
--- /dev/null
+++ b/addons/fleet/models/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import fleet_vehicle
+from . import fleet_vehicle_cost
+from . import fleet_vehicle_model
+from . import res_config_settings
+from . import res_partner
diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py
new file mode 100644
index 00000000..8cb15b38
--- /dev/null
+++ b/addons/fleet/models/fleet_vehicle.py
@@ -0,0 +1,397 @@
+# -*- 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, _
+from odoo.osv import expression
+
+
+class FleetVehicle(models.Model):
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _name = 'fleet.vehicle'
+ _description = 'Vehicle'
+ _order = 'license_plate asc, acquisition_date asc'
+
+ 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.Text("Vehicle Description")
+ active = fields.Boolean('Active', default=True, tracking=True)
+ company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_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)
+ driver_id = fields.Many2one('res.partner', 'Driver', tracking=True, help='Driver of the vehicle', copy=False)
+ future_driver_id = fields.Many2one('res.partner', 'Future Driver', tracking=True, help='Next Driver 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, help='Model of the vehicle')
+ manager_id = fields.Many2one('res.users', compute='_compute_manager_id', domain=lambda self: [('groups_id', 'in', self.env.ref('fleet.fleet_group_manager').id)], store=True, readonly=False)
+
+ @api.depends('model_id')
+ def _compute_manager_id(self):
+ for vehicle in self:
+ if vehicle.model_id:
+ vehicle.manager_id = vehicle.model_id.manager_id
+ else:
+ vehicle.manager_id = None
+
+ 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('Immatriculation Date', required=False,
+ default=fields.Date.today, help='Date when the vehicle has been immatriculated')
+ first_contract_date = fields.Date(string="First Contract Date", default=fields.Date.today)
+ color = fields.Char(help='Color of the vehicle')
+ 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')
+ model_year = fields.Char('Model Year', help='Year of the model')
+ 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', 'km'),
+ ('miles', 'mi')
+ ], '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'),
+ ('lpg', 'LPG'),
+ ('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_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')
+ car_value = fields.Float(string="Catalog Value (VAT Incl.)", help='Value of the bought vehicle')
+ net_car_value = fields.Float(string="Purchase Value", help="Purchase value of the vehicle")
+ residual_value = fields.Float()
+ plan_to_change_car = fields.Boolean(related='driver_id.plan_to_change_car', store=True, readonly=False)
+ vehicle_type = fields.Selection(related='model_id.vehicle_type')
+
+ @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']
+ LogContract = self.env['fleet.vehicle.log.contract']
+ for record in self:
+ record.odometer_count = Odometer.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', '!=', 'closed')])
+ record.history_count = self.env['fleet.vehicle.assignation.log'].search_count([('vehicle_id', '=', record.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 = ''
+ 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
+
+ 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 _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('id')
+ 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('id')
+ res.append(('id', search_operator, res_ids))
+ return res
+
+ @api.model
+ def create(self, vals):
+ # Fleet administrator may not have rights to create the plan_to_change_car value when the driver_id is a res.user
+ # This trick is used to prevent access right error.
+ ptc_value = 'plan_to_change_car' in vals.keys() and {'plan_to_change_car': vals.pop('plan_to_change_car')}
+ res = super(FleetVehicle, self).create(vals)
+ if ptc_value:
+ res.sudo().write(ptc_value)
+ if 'driver_id' in vals and vals['driver_id']:
+ res.create_driver_history(vals['driver_id'])
+ 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 = res.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'])
+ future_driver.sudo().write({'plan_to_change_car': True})
+ return res
+
+ def write(self, vals):
+ if 'driver_id' in vals and vals['driver_id']:
+ driver_id = vals['driver_id']
+ self.filtered(lambda v: v.driver_id.id != driver_id).create_driver_history(driver_id)
+
+ 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'])
+ future_driver.sudo().write({'plan_to_change_car': True})
+
+ res = super(FleetVehicle, self).write(vals)
+ if 'active' in vals and not vals['active']:
+ self.mapped('log_contracts').write({'active': False})
+ return res
+
+ def _close_driver_history(self):
+ self.env['fleet.vehicle.assignation.log'].search([
+ ('vehicle_id', 'in', self.ids),
+ ('driver_id', 'in', self.mapped('driver_id').ids),
+ ('date_end', '=', False)
+ ]).write({'date_end': fields.Date.today()})
+
+ def create_driver_history(self, driver_id):
+ for vehicle in self:
+ self.env['fleet.vehicle.assignation.log'].create({
+ 'vehicle_id': vehicle.id,
+ 'driver_id': driver_id,
+ 'date_start': fields.Date.today(),
+ })
+
+ def action_accept_driver_change(self):
+ self._close_driver_history()
+ # Find all the vehicles 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)])
+ vehicles.write({'driver_id': False})
+ vehicles._close_driver_history()
+
+ for vehicle in self:
+ 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)
+
+ @api.model
+ def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
+ args = args or []
+ if operator == 'ilike' and not (name or '').strip():
+ domain = []
+ else:
+ domain = ['|', ('name', operator, name), ('driver_id.name', operator, name)]
+ return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)
+
+ 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}
+ }
+
+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
+
+
+class FleetVehicleState(models.Model):
+ _name = 'fleet.vehicle.state'
+ _order = 'sequence asc'
+ _description = 'Vehicle Status'
+
+ name = fields.Char(required=True, translate=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'
+ _description = 'Vehicle Tag'
+
+ name = fields.Char('Tag Name', required=True, translate=True)
+ color = fields.Integer('Color Index')
+
+ _sql_constraints = [('name_uniq', 'unique (name)', "Tag name already exists !")]
+
+
+class FleetServiceType(models.Model):
+ _name = 'fleet.service.type'
+ _description = 'Fleet Service Type'
+
+ 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')
+
+
+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")
diff --git a/addons/fleet/models/fleet_vehicle_cost.py b/addons/fleet/models/fleet_vehicle_cost.py
new file mode 100644
index 00000000..266cf94a
--- /dev/null
+++ b/addons/fleet/models/fleet_vehicle_cost.py
@@ -0,0 +1,189 @@
+# -*- 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 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, help='Vehicle concerned by this log')
+ cost_subtype_id = fields.Many2one('fleet.service.type', 'Type', help='Cost type purchased with this cost', domain=[('category', '=', 'contract')])
+ amount = fields.Monetary('Cost')
+ 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)
+ 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')
+ insurer_id = fields.Many2one('res.partner', 'Vendor')
+ purchaser_id = fields.Many2one(related='vehicle_id.driver_id', string='Current 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.Text('Terms and Conditions', help='Write here all supplementary information relative to this contract', copy=False)
+ cost_generated = fields.Monetary('Recurring Cost')
+ cost_frequency = fields.Selection([
+ ('no', 'No'),
+ ('daily', 'Daily'),
+ ('weekly', 'Weekly'),
+ ('monthly', 'Monthly'),
+ ('yearly', 'Yearly')
+ ], 'Recurring Cost Frequency', default='monthly', help='Frequency of the recuring cost', 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
+ """
+ for record in self:
+ if record.expiration_date and record.state in ['open', 'expired']:
+ 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
+
+ def write(self, vals):
+ res = super(FleetVehicleLogContract, self).write(vals)
+ 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 contract_close(self):
+ for record in self:
+ record.state = 'closed'
+
+ def contract_draft(self):
+ for record in self:
+ record.state = 'futur'
+
+ def contract_open(self):
+ for record in self:
+ record.state = 'open'
+
+ @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))
+ nearly_expired_contracts = self.search([('state', '=', 'open'), ('expiration_date', '<', outdated_days)])
+
+ for contract in nearly_expired_contracts.filtered(lambda contract: contract.user_id):
+ 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()
+
+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, help='Vehicle concerned by this log')
+ 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_8', raise_if_not_found=False),
+ )
+ state = fields.Selection([
+ ('todo', 'To Do'),
+ ('running', 'Running'),
+ ('done', 'Done'),
+ ('cancelled', 'Cancelled'),
+ ], default='todo', string='Stage')
+
+ 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
diff --git a/addons/fleet/models/fleet_vehicle_model.py b/addons/fleet/models/fleet_vehicle_model.py
new file mode 100644
index 00000000..48f36492
--- /dev/null
+++ b/addons/fleet/models/fleet_vehicle_model.py
@@ -0,0 +1,66 @@
+# -*- 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', 'Manufacturer', required=True, help='Manufacturer of the vehicle')
+ vendors = fields.Many2many('res.partner', 'fleet_vehicle_model_vendors', 'model_id', 'partner_id', string='Vendors')
+ manager_id = fields.Many2one('res.users', 'Fleet Manager', default=lambda self: self.env.uid,
+ domain=lambda self: [('groups_id', 'in', self.env.ref('fleet.fleet_group_manager').id)])
+ 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)
+
+ @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
+
+ def write(self, vals):
+ if 'manager_id' in vals:
+ old_manager = self.manager_id.id if self.manager_id else None
+
+ self.env['fleet.vehicle'].search([('model_id', '=', self.id), ('manager_id', '=', old_manager)]).write({'manager_id': vals['manager_id']})
+
+ return super(FleetVehicleModel, self).write(vals)
+
+
+class FleetVehicleModelBrand(models.Model):
+ _name = 'fleet.vehicle.model.brand'
+ _description = 'Brand of the vehicle'
+ _order = 'model_count desc, name asc'
+
+ name = fields.Char('Make', 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 = self.env['fleet.vehicle.model']
+ for record in self:
+ record.model_count = Model.search_count([('brand_id', '=', record.id)])
+
+ 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
diff --git a/addons/fleet/models/res_config_settings.py b/addons/fleet/models/res_config_settings.py
new file mode 100644
index 00000000..59b725d8
--- /dev/null
+++ b/addons/fleet/models/res_config_settings.py
@@ -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 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')
+ module_fleet_account = fields.Boolean(string="Analytic Accounting Fleet")
diff --git a/addons/fleet/models/res_partner.py b/addons/fleet/models/res_partner.py
new file mode 100644
index 00000000..e2cb7c1b
--- /dev/null
+++ b/addons/fleet/models/res_partner.py
@@ -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 ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ plan_to_change_car = fields.Boolean('Plan To Change Car', default=False)