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

+ Click to create a new cost. +

+ Odoo helps you managing the costs for your different + vehicles. Costs are created automatically from services, + contracts (fixed or recurring) and fuel logs. +

+
+
+ + + + + + fleet.vehicle.log_contract.form + fleet.vehicle.log.contract + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + + + + + + + + + + +
+ + + +
+
+
+
+ + + + + + fleet.vehicle.log.contract.tree + fleet.vehicle.log.contract + + + + + + + + + + + + + + + + + + + fleet.vehicle.log.contract.graph + fleet.vehicle.log.contract + + + + + + + + + + + fleet.vehicle.log.contract.search + fleet.vehicle.log.contract + + + + + + + + + + Vehicles Contracts + fleet.vehicle.log.contract + form + tree,form,graph + {'search_default_open': 1} + +

+ Click to create a new contract. +

+ Manage all your contracts (leasing, insurances, etc.) with + their related services, costs. Odoo will automatically warn + you when some contracts have to be renewed. +

+ Each contract (e.g.: leasing) may include several services + (reparation, insurances, periodic maintenance). +

+
+
+ + + + + fleet.vehicle.log.fuel.form + fleet.vehicle.log.fuel + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + fleet.vehicle.log.fuel.tree + fleet.vehicle.log.fuel + + + + + + + + + + + + + + + + + + fleet.vehicle.log.fuel.search + fleet.vehicle.log.fuel + + + + + + + + + + + + + + + + fleet.vehicle.log.fuel.graph + fleet.vehicle.log.fuel + + + + + + + + + + Vehicles Fuel Logs + fleet.vehicle.log.fuel + form + tree,form,graph + {"search_default_groupby_vehicle" : True} + +

+ Click to create a new fuel log. +

+ Here you can add refuelling entries for all vehicles. You can + also filter logs of a particular vehicle using the search + field. +

+
+
+ + + + + fleet.vehicle.log.services.form + fleet.vehicle.log.services + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + fleet.vehicle.log.services.tree + fleet.vehicle.log.services + + + + + + + + + + + + + + + + fleet.vehicle.log.services.graph + fleet.vehicle.log.services + + + + + + + + + + + fleet.vehicle.log.services.search + fleet.vehicle.log.services + + + + + + + + + + Vehicles Services Logs + fleet.vehicle.log.services + form + tree,form,graph + +

+ Click to create a new service entry. +

+ Odoo helps you keeping track of all the services done + on your vehicle. Services can be of many type: occasional + repair, fixed maintenance, etc. +

+
+
+ + + +
diff --git a/addons/fleet/views/fleet_vehicle_model_views.xml b/addons/fleet/views/fleet_vehicle_model_views.xml new file mode 100644 index 00000000000..da4027f01d7 --- /dev/null +++ b/addons/fleet/views/fleet_vehicle_model_views.xml @@ -0,0 +1,158 @@ + + + + fleet.vehicle.model.form + fleet.vehicle.model + +
+ + +
+
+ + + + + + + +
+ X +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + fleet.vehicle.model.tree + fleet.vehicle.model + + + + + + + + + + fleet.vehicle.model.search + fleet.vehicle.model + + + + + + + + + + + + Vehicle Model + fleet.vehicle.model + form + tree,form + {"search_default_groupby_brand" : True,} + +

+ Click to create a new model. +

+ You can define several models (e.g. A3, A4) for each make (Audi). +

+
+
+ + + + + + + fleet.vehicle.model.brand.tree + fleet.vehicle.model.brand + + + + + + + + + fleet.vehicle.model.brand.form + fleet.vehicle.model.brand + +
+ +
+ +
+
+
+
+
+ + + fleet.vehicle.model.brandkanban + fleet.vehicle.model.brand + + + + + + + +
+ + + +
+

+ + + +

+
+
+
+
+
+
+
+ + + Model make of Vehicle + fleet.vehicle.model.brand + form + kanban,tree,form + +

+ Click to create a new make. +

+
+
+ + +
diff --git a/addons/fleet/views/fleet_vehicle_views.xml b/addons/fleet/views/fleet_vehicle_views.xml new file mode 100644 index 00000000000..4a5416968d7 --- /dev/null +++ b/addons/fleet/views/fleet_vehicle_views.xml @@ -0,0 +1,400 @@ + + + + + fleet.vehicle.form + fleet.vehicle + +
+
+ +
+ +
+ + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + fleet.vehicle.tree + fleet.vehicle + + + + + + + + + + + + + + + + + + + + fleet.vehicle.search + fleet.vehicle + + + + + + + + + + + + + + + + + + + + + fleet.vehicle.kanban + fleet.vehicle + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ + : + +
    +
  • + +
  • +
  • + +
  • +
+ +
+
+
+
+
+
+
+ + + + Vehicles + fleet.vehicle + form + kanban,tree,form + +

+ Click to create a new vehicle. +

+ You will be able to manage your fleet by keeping track of the + contracts, services, fixed and recurring costs, odometers and + fuel logs associated to each vehicle. +

+ Odoo will warn you when services or contract have to be + renewed. +

+
+
+ + + + + + fleet.vehicle.odometer.form + fleet.vehicle.odometer + +
+ + + +
+ + +
+ +
+
+
+
+
+ + + fleet.vehicle.odometer.tree + fleet.vehicle.odometer + + + + + + + + + + + + + fleet.vehicle.odometer.search + fleet.vehicle.odometer + + + + + + + + + + + + + + + fleet.vehicle.odometer.graph + fleet.vehicle.odometer + + + + + + + + + + Vehicles Odometer + fleet.vehicle.odometer + form + tree,graph + {"search_default_groupby_vehicle" : True} + +

+ Click to create a new odometer log. +

+

+ Here you can add various odometer entries for all vehicles. + You can also show odometer value for a particular vehicle using + the search field. +

+
+
+ + + + + fleet.service.type.tree + fleet.service.type + + + + + + + + + + Service Types + fleet.service.type + form + tree,form + +

+ Click to create a new type of service. +

+ Each service can used in contracts, as a standalone service or both. +

+
+
+ + + + + fleet.vehicle.state.tree + fleet.vehicle.state + + + + + + + + + + + Vehicle Status + fleet.vehicle.state + form + tree,form + +

+ Click to create a vehicle status. +

+ You can customize available status to track the evolution of + each vehicle. Example: Active, Being Repaired, Sold. +

+
+
+ + + + + fleet.vehicle.tag.form + fleet.vehicle.tag + +
+ + + +
+
+
+ + + Vehicle Tags + fleet.vehicle.tag + form + +

+ Click to add a new tag. +

+
+
+ + + +
\ No newline at end of file diff --git a/addons/fleet/views/fleet_view.xml b/addons/fleet/views/fleet_view.xml deleted file mode 100644 index 0a4a9fe58fb..00000000000 --- a/addons/fleet/views/fleet_view.xml +++ /dev/null @@ -1,1065 +0,0 @@ - - - - fleet.vehicle.model.form - fleet.vehicle.model - -
- - -
-
- - - - - - - -
- X -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
- - - fleet.vehicle.model.tree - fleet.vehicle.model - - - - - - - - - - fleet.vehicle.model.search - fleet.vehicle.model - - - - - - - - - - - - Vehicle Model - fleet.vehicle.model - form - tree,form - {"search_default_groupby_brand" : True,} - -

- Click to create a new model. -

- You can define several models (e.g. A3, A4) for each make (Audi). -

-
-
- - - fleet.vehicle.model.brand.tree - fleet.vehicle.model.brand - - - - - - - - - fleet.vehicle.model.brand.form - fleet.vehicle.model.brand - -
- -
- -
-
-
-
-
- - - fleet.vehicle.model.brandkanban - fleet.vehicle.model.brand - - - - - - - -
- - - -
-

- - - -

-
-
-
-
-
-
-
- - - Model make of Vehicle - fleet.vehicle.model.brand - form - kanban,tree,form - -

- Click to create a new make. -

-
-
- - - fleet.vehicle.state.tree - fleet.vehicle.state - - - - - - - - - - - Vehicle Status - fleet.vehicle.state - form - tree,form - -

- Click to create a vehicle status. -

- You can customize available status to track the evolution of - each vehicle. Example: Active, Being Repaired, Sold. -

-
-
- - - - - - - - - fleet.vehicle.form - fleet.vehicle - -
-
- -
- -
- - - - - - -
- -
-
- - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - fleet.vehicle.tree - fleet.vehicle - - - - - - - - - - - - - - - - - - - - fleet.vehicle.search - fleet.vehicle - - - - - - - - - - - - - - - - - - - - - fleet.vehicle.kanban - fleet.vehicle - - - - - - - - - - - - - - - - - -
-
- -
-
- -
-
- - : - -
    -
  • - -
  • -
  • - -
  • -
- -
-
-
-
-
-
-
- - - - Vehicles - fleet.vehicle - form - kanban,tree,form - -

- Click to create a new vehicle. -

- You will be able to manage your fleet by keeping track of the - contracts, services, fixed and recurring costs, odometers and - fuel logs associated to each vehicle. -

- Odoo will warn you when services or contract have to be - renewed. -

-
-
- - - - - - - - fleet.vehicle.log_contract.form - fleet.vehicle.log.contract - -
-
-
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
-
- - - - - - - - - - - -
- - - -
-
-
-
- - - - - - fleet.vehicle.log.contract.tree - fleet.vehicle.log.contract - - - - - - - - - - - - - - - - - - - fleet.vehicle.log.contract.graph - fleet.vehicle.log.contract - - - - - - - - - - - fleet.vehicle.log.contract.search - fleet.vehicle.log.contract - - - - - - - - - - Vehicles Contracts - fleet.vehicle.log.contract - form - tree,form,graph - {'search_default_open': 1} - -

- Click to create a new contract. -

- Manage all your contracts (leasing, insurances, etc.) with - their related services, costs. Odoo will automatically warn - you when some contracts have to be renewed. -

- Each contract (e.g.: leasing) may include several services - (reparation, insurances, periodic maintenance). -

-
-
- - - - - fleet.vehicle.odometer.form - fleet.vehicle.odometer - -
- - - -
- - -
- -
-
-
-
-
- - - fleet.vehicle.odometer.tree - fleet.vehicle.odometer - - - - - - - - - - - - - fleet.vehicle.odometer.search - fleet.vehicle.odometer - - - - - - - - - - - - - - - fleet.vehicle.odometer.graph - fleet.vehicle.odometer - - - - - - - - - - Vehicles Odometer - fleet.vehicle.odometer - form - tree,graph - {"search_default_groupby_vehicle" : True} - -

- Click to create a new odometer log. -

-

- Here you can add various odometer entries for all vehicles. - You can also show odometer value for a particular vehicle using - the search field. -

-
-
- - - - - fleet.vehicle.log.fuel.form - fleet.vehicle.log.fuel - -
- - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - fleet.vehicle.log.fuel.tree - fleet.vehicle.log.fuel - - - - - - - - - - - - - - - - - - fleet.vehicle.log.fuel.search - fleet.vehicle.log.fuel - - - - - - - - - - - - - - - - fleet.vehicle.log.fuel.graph - fleet.vehicle.log.fuel - - - - - - - - - - Vehicles Fuel Logs - fleet.vehicle.log.fuel - form - tree,form,graph - {"search_default_groupby_vehicle" : True} - -

- Click to create a new fuel log. -

- Here you can add refuelling entries for all vehicles. You can - also filter logs of a particular vehicle using the search - field. -

-
-
- - - - - - fleet.vehicle.log.services.form - fleet.vehicle.log.services - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - fleet.vehicle.log.services.tree - fleet.vehicle.log.services - - - - - - - - - - - - - - - - fleet.vehicle.log.services.graph - fleet.vehicle.log.services - - - - - - - - - - - fleet.vehicle.log.services.search - fleet.vehicle.log.services - - - - - - - - - - Vehicles Services Logs - fleet.vehicle.log.services - form - tree,form,graph - -

- Click to create a new service entry. -

- Odoo helps you keeping track of all the services done - on your vehicle. Services can be of many type: occasional - repair, fixed maintenance, etc. -

-
-
- - - - - fleet.vehicle.tag.form - fleet.vehicle.tag - -
- - - -
-
-
- - - Vehicle Tags - fleet.vehicle.tag - form - -

- Click to add a new tag. -

-
-
- - - - - fleet.service.type.tree - fleet.service.type - - - - - - - - - - - Service Types - fleet.service.type - form - tree,form - -

- Click to create a new type of service. -

- Each service can used in contracts, as a standalone service or both. -

-
-
- - - - - fleet.vehicle.cost.tree - fleet.vehicle.cost - - - - - - - - - - - - fleet.vehicle.cost.pivot - fleet.vehicle.cost - - - - - - - - - - - - fleet.vehicle.cost.graph - fleet.vehicle.cost - - - - - - - - - - - fleet.vehicle.cost.pivot - fleet.vehicle.cost - - - - - - - - - - - fleet.vehicle.cost.graph - fleet.vehicle.cost - - - - - - - - - - - fleet.vehicle.cost.search - fleet.vehicle.cost - - - - - - - - - - - - - - - - - - - - - - - fleet.vehicle.cost.form - fleet.vehicle.cost - -
- - - - - - - - - - - - - - -
-
-
- - - fleet.vehicle.cost.graph - fleet.vehicle.cost - - - - - - - - - - - Vehicle Costs - fleet.vehicle.cost - form - tree,form,graph - {"search_default_parent_false" : True, "search_default_groupby_vehicle_id" : True,} - -

- Click to create a new cost. -

- Odoo helps you managing the costs for your different - vehicles. Costs are created automatically from services, - contracts (fixed or recurring) and fuel logs. -

-
-
- - -