Browse Source

Dec 30 : [UPDT] Bug Fixed 'fleet_rental'

pull/299/head
AjmalCybro 1 year ago
parent
commit
16507985cc
  1. 8
      fleet_rental/__manifest__.py
  2. 6
      fleet_rental/doc/RELEASE_NOTES.md
  3. 10
      fleet_rental/models/__init__.py
  4. 47
      fleet_rental/models/account_move.py
  5. 37
      fleet_rental/models/account_move_line.py
  6. 460
      fleet_rental/models/car_rental.py
  7. 40
      fleet_rental/models/car_rental_checklist.py
  8. 30
      fleet_rental/models/car_tools.py
  9. 21
      fleet_rental/models/fleet.py
  10. 55
      fleet_rental/models/fleet_rental_line.py
  11. 45
      fleet_rental/models/fleet_vehicle.py
  12. 39
      fleet_rental/models/res_config_settings.py
  13. 2
      fleet_rental/reports/__init__.py
  14. 21
      fleet_rental/reports/rental_report.py
  15. 14
      fleet_rental/reports/rental_report.xml
  16. 32
      fleet_rental/static/src/js/time_widget.js
  17. 3
      fleet_rental/static/src/scss/timepicker.scss
  18. 9
      fleet_rental/static/src/xml/timepicker.xml
  19. 91
      fleet_rental/views/car_rental_view.xml
  20. 15
      fleet_rental/views/checklist_view.xml
  21. 27
      fleet_rental/views/res_config_settings_views.xml

8
fleet_rental/__manifest__.py

@ -38,8 +38,16 @@
'views/car_rental_view.xml', 'views/car_rental_view.xml',
'views/checklist_view.xml', 'views/checklist_view.xml',
'views/car_tools_view.xml', 'views/car_tools_view.xml',
'views/res_config_settings_views.xml',
'reports/rental_report.xml' 'reports/rental_report.xml'
], ],
"assets": {
'web.assets_backend': [
'fleet_rental/static/src/js/time_widget.js',
'fleet_rental/static/src/xml/timepicker.xml',
'fleet_rental/static/src/scss/timepicker.scss'
],
},
'demo': [ 'demo': [
], ],
'images': ['static/description/banner.png'], 'images': ['static/description/banner.png'],

6
fleet_rental/doc/RELEASE_NOTES.md

@ -5,6 +5,8 @@
#### ADD #### ADD
Initial Commit for Fleet Rental Management Initial Commit for Fleet Rental Management
#### 22.12.2023
#### Version 16.0.2.1.1
#### UPDT
- Reformatted code for hourly payment, invoice cancellation, extend rent.

10
fleet_rental/models/__init__.py

@ -3,7 +3,7 @@
# #
# Cybrosys Technologies Pvt. Ltd. # Cybrosys Technologies Pvt. Ltd.
# #
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). # Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com) # Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
# #
# You can modify it under the terms of the GNU AFFERO # You can modify it under the terms of the GNU AFFERO
@ -19,6 +19,12 @@
# If not, see <http://www.gnu.org/licenses/>. # If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################# #############################################################################
from . import account_move
from . import account_move_line
from . import car_rental from . import car_rental
from . import car_rental_checklist
from . import car_tools
from . import fleet from . import fleet
from . import fleet_rental_line
from . import fleet_vehicle
from . import res_config_settings

47
fleet_rental/models/account_move.py

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class AccountMove(models.Model):
"""Inherit account.move to add extra field """
_inherit = 'account.move'
fleet_rent_id = fields.Many2one('car.rental.contract',
string='Rental',
help='Invoice related to which '
'rental record')
def button_cancel(self):
"""
Override the base method button_cancel to handle additional logic
for 'car.rental.contract' model based on 'fleet_rent_id'.
"""
res = super().button_cancel()
fleet_model = self.env['car.rental.contract'].search(
[('id', '=', self.fleet_rent_id.id)])
if fleet_model.state == 'running':
fleet_model.state = 'running'
fleet_model.first_invoice_created = False
else:
fleet_model.state = 'checking'
return res

37
fleet_rental/models/account_move_line.py

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import api, models
class AccountMoveLine(models.Model):
"""Inherit account.move.line"""
_inherit = 'account.move.line'
@api.onchange('price_unit')
def _onchange_price_unit(self):
"""
Update the 'first_payment' field of the associated
'car.rental.contract' model when the 'price_unit' field changes.
"""
fleet_model = self.move_id.fleet_rent_id
if fleet_model:
fleet_model.first_payment = self.price_unit

460
fleet_rental/models/car_rental.py

@ -3,7 +3,7 @@
# #
# Cybrosys Technologies Pvt. Ltd. # Cybrosys Technologies Pvt. Ltd.
# #
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). # Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com) # Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
# #
# You can modify it under the terms of the GNU AFFERO # You can modify it under the terms of the GNU AFFERO
@ -19,9 +19,8 @@
# If not, see <http://www.gnu.org/licenses/>. # If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################# #############################################################################
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from odoo import models, fields, api, _ from odoo import api, fields, models, _
from odoo.exceptions import UserError, Warning, ValidationError from odoo.exceptions import UserError, Warning, ValidationError
@ -30,89 +29,124 @@ class CarRentalContract(models.Model):
_description = 'Fleet Rental Management' _description = 'Fleet Rental Management'
_inherit = 'mail.thread' _inherit = 'mail.thread'
@api.onchange('rent_start_date', 'rent_end_date') image = fields.Binary(related='vehicle_id.image_128',
def check_availability(self): string="Image of Vehicle")
self.vehicle_id = '' reserved_fleet_id = fields.Many2one('rental.fleet.reserved',
fleet_obj = self.env['fleet.vehicle'].search([]) invisible=True, copy=False)
for i in fleet_obj: name = fields.Char(string="Name", default="Draft Contract", readonly=True,
for each in i.rental_reserved_time: copy=False)
customer_id = fields.Many2one('res.partner', required=True,
if str(each.date_from) <= str(self.rent_start_date) <= str(each.date_to): string='Customer', help="Customer")
i.write({'rental_check_availability': False}) vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle",
elif str(self.rent_start_date) < str(each.date_from): required=True, help="Vehicle",
if str(each.date_from) <= str(self.rent_end_date) <= str(each.date_to):
i.write({'rental_check_availability': False})
elif str(self.rent_end_date) > str(each.date_to):
i.write({'rental_check_availability': False})
else:
i.write({'rental_check_availability': True})
else:
i.write({'rental_check_availability': True})
image = fields.Binary(related='vehicle_id.image_128', string="Image of Vehicle")
reserved_fleet_id = fields.Many2one('rental.fleet.reserved', invisible=True, copy=False)
name = fields.Char(string="Name", default="Draft Contract", readonly=True, copy=False)
customer_id = fields.Many2one('res.partner', required=True, string='Customer', help="Customer")
vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True, help="Vehicle",
readonly=True, readonly=True,
states={'draft': [('readonly', False)]} states={'draft': [('readonly', False)]}
) )
car_brand = fields.Many2one('fleet.vehicle.model.brand', string="Fleet Brand", size=50, car_brand = fields.Many2one('fleet.vehicle.model.brand',
related='vehicle_id.model_id.brand_id', store=True, readonly=True) string="Fleet Brand", size=50,
car_color = fields.Char(string="Fleet Color", size=50, related='vehicle_id.color', store=True, copy=False, related='vehicle_id.model_id.brand_id',
store=True, readonly=True)
car_color = fields.Char(string="Fleet Color", size=50,
related='vehicle_id.color', store=True, copy=False,
default='#FFFFFF', readonly=True) default='#FFFFFF', readonly=True)
cost = fields.Float(string="Rent Cost", help="This fields is to determine the cost of rent", required=True) cost = fields.Float(string="Rent Cost",
rent_start_date = fields.Date(string="Rent Start Date", required=True, default=str(date.today()), help="This fields is to determine the cost of rent",
help="Start date of contract", track_visibility='onchange') required=True)
rent_end_date = fields.Date(string="Rent End Date", required=True, help="End date of contract", rent_start_date = fields.Date(string="Rent Start Date", required=True,
default=str(date.today()),
help="Start date of contract",
track_visibility='onchange')
start_time = fields.Char(string="Start By",
help="Enter the contract starting hour")
end_time = fields.Char(string="End By",
help="Enter the contract Ending hour")
rent_by_hour = fields.Boolean(string="Rent By Hour",
help="Enable to start contract on "
"hour basis")
rent_end_date = fields.Date(string="Rent End Date", required=True,
help="End date of contract",
track_visibility='onchange') track_visibility='onchange')
state = fields.Selection( state = fields.Selection(
[('draft', 'Draft'), ('reserved', 'Reserved'), ('running', 'Running'), ('cancel', 'Cancel'), [('draft', 'Draft'), ('reserved', 'Reserved'), ('running', 'Running'),
('checking', 'Checking'), ('invoice', 'Invoice'), ('done', 'Done')], string="State", ('cancel', 'Cancel'),
('checking', 'Checking'), ('invoice', 'Invoice'), ('done', 'Done')],
string="State",
default="draft", copy=False, track_visibility='onchange') default="draft", copy=False, track_visibility='onchange')
notes = fields.Text(string="Details & Notes") notes = fields.Text(string="Details & Notes")
cost_generated = fields.Float(string='Recurring Cost', cost_generated = fields.Float(string='Recurring Cost',
help="Costs paid at regular intervals, depending on the cost frequency") help="Costs paid at regular intervals, depending on the cost frequency")
cost_frequency = fields.Selection([('no', 'No'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), cost_frequency = fields.Selection(
[('no', 'No'), ('daily', 'Daily'), ('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly')], string="Recurring Cost Frequency", ('yearly', 'Yearly')], string="Recurring Cost Frequency",
help='Frequency of the recurring cost', required=True) help='Frequency of the recurring cost', required=True)
journal_type = fields.Many2one('account.journal', 'Journal', journal_type = fields.Many2one('account.journal', 'Journal',
default=lambda self: self.env['account.journal'].search([('id', '=', 1)])) default=lambda self: self.env[
'account.journal'].search(
[('id', '=', 1)]))
account_type = fields.Many2one('account.account', 'Account', account_type = fields.Many2one('account.account', 'Account',
default=lambda self: self.env['account.account'].search([('id', '=', 17)])) default=lambda self: self.env[
recurring_line = fields.One2many('fleet.rental.line', 'rental_number', readonly=True, help="Recurring Invoices", 'account.account'].search(
[('id', '=', 17)]))
recurring_line = fields.One2many('fleet.rental.line', 'rental_number',
readonly=True, help="Recurring Invoices",
copy=False) copy=False)
first_payment = fields.Float(string='First Payment', first_payment = fields.Float(string='First Payment',
help="Transaction/Office/Contract charge amount, must paid by customer side other " help="Transaction/Office/Contract charge "
"amount, must paid by customer side "
"other "
"than recurrent payments", "than recurrent payments",
track_visibility='onchange', track_visibility='onchange',
required=True) required=True)
first_payment_inv = fields.Many2one('account.move', copy=False) first_payment_inv = fields.Many2one('account.move', copy=False)
first_invoice_created = fields.Boolean(string="First Invoice Created", invisible=True, copy=False) first_invoice_created = fields.Boolean(string="First Invoice Created",
attachment_ids = fields.Many2many('ir.attachment', 'car_rent_checklist_ir_attachments_rel', invisible=True, copy=False)
'rental_id', 'attachment_id', string="Attachments", attachment_ids = fields.Many2many('ir.attachment',
help="Images of the vehicle before contract/any attachments") 'car_rent_checklist_ir_attachments_rel',
checklist_line = fields.One2many('car.rental.checklist', 'checklist_number', string="Checklist", 'rental_id', 'attachment_id',
help="Facilities/Accessories, That should verify when closing the contract.", string="Attachments",
help="Images of the vehicle before "
"contract/any attachments")
checklist_line = fields.One2many('car.rental.checklist',
'checklist_number', string="Checklist",
help="Facilities/Accessories, That should"
" verify when closing the contract.",
states={'invoice': [('readonly', True)], states={'invoice': [('readonly', True)],
'done': [('readonly', True)], 'done': [('readonly', True)],
'cancel': [('readonly', True)]}) 'cancel': [('readonly', True)]})
total = fields.Float(string="Total (Accessories/Tools)", readonly=True, copy=False) total = fields.Float(string="Total (Accessories/Tools)", readonly=True,
tools_missing_cost = fields.Float(string="Missing Cost", readonly=True, copy=False, copy=False)
help='This is the total amount of missing tools/accessories') tools_missing_cost = fields.Float(string="Missing Cost", readonly=True,
damage_cost = fields.Float(string="Damage Cost", copy=False) copy=False,
damage_cost_sub = fields.Float(string="Damage Cost", readonly=True, copy=False) help='This is the total amount of '
'missing tools/accessories')
damage_cost = fields.Float(string="Damage Cost / Balance Amount",
copy=False)
damage_cost_sub = fields.Float(string="Damage Cost / Balance Amount",
readonly=True,
copy=False)
total_cost = fields.Float(string="Total", readonly=True, copy=False) total_cost = fields.Float(string="Total", readonly=True, copy=False)
invoice_count = fields.Integer(compute='_invoice_count', string='# Invoice', copy=False) invoice_count = fields.Integer(compute='_invoice_count',
string='# Invoice', copy=False)
check_verify = fields.Boolean(compute='check_action_verify', copy=False) check_verify = fields.Boolean(compute='check_action_verify', copy=False)
sales_person = fields.Many2one('res.users', string='Sales Person', default=lambda self: self.env.uid, sales_person = fields.Many2one('res.users', string='Sales Person',
default=lambda self: self.env.uid,
track_visibility='always') track_visibility='always')
read_only = fields.Boolean(string="Read Only", help="To make field read "
"only")
def action_run(self): def action_run(self):
"""
Set the state of the object to 'running'.
"""
self.state = 'running' self.state = 'running'
@api.depends('checklist_line.checklist_active') @api.depends('checklist_line.checklist_active')
def check_action_verify(self): def check_action_verify(self):
"""
Update the 'check_verify' field based on the value of
'checklist_active' in 'checklist_line'.
"""
flag = 0 flag = 0
for each in self: for each in self:
for i in each.checklist_line: for i in each.checklist_line:
@ -127,27 +161,44 @@ class CarRentalContract(models.Model):
@api.constrains('rent_start_date', 'rent_end_date') @api.constrains('rent_start_date', 'rent_end_date')
def validate_dates(self): def validate_dates(self):
"""
Check the validity of the 'rent_start_date' and 'rent_end_date'
fields.
Raise a warning if 'rent_end_date' is earlier than
'rent_start_date'.
"""
if self.rent_end_date < self.rent_start_date: if self.rent_end_date < self.rent_start_date:
raise ValidationError("Please select the valid end date.") raise UserError("Please select the valid end date.")
def set_to_done(self): def set_to_done(self):
invoice_ids = self.env['account.move'].search([('invoice_origin', '=', self.name)]) """
f = 0 Set the state of the object to 'done' based on certain conditions related to invoices.
for each in invoice_ids: Raise a UserError if certain invoices are pending or the total cost is zero.
if each.payment_state != 'paid': """
f = 1 invoice_ids = self.env['account.move'].search(
break [('fleet_rent_id', '=', self.id),
if f == 0: ('amount_untaxed_signed', '=', self.total_cost),
('invoice_line_ids.name', '=', 'Damage/Tools missing cost')])
if any(each.payment_state == 'paid' for each in
invoice_ids) or self.total_cost == 0:
self.state = 'done' self.state = 'done'
else: else:
raise UserError("Some Invoices are pending") raise UserError("Some Invoices are pending")
def _invoice_count(self): def _invoice_count(self):
invoice_ids = self.env['account.move'].search([('invoice_origin', '=', self.name)]) """
self.invoice_count = len(invoice_ids) Calculate the count of invoices related to the current object.
Update the 'invoice_count' field accordingly.
"""
self.invoice_count = self.env['account.move'].search_count(
[('fleet_rent_id', '=', self.id)])
@api.constrains('state') @api.constrains('state')
def state_changer(self): def state_changer(self):
"""
Handle state transitions and update the 'state_id' of the
associated vehicle based on the value of the 'state' field.
"""
if self.state == "running": if self.state == "running":
state_id = self.env.ref('fleet_rental.vehicle_state_rent').id state_id = self.env.ref('fleet_rental.vehicle_state_rent').id
self.vehicle_id.write({'state_id': state_id}) self.vehicle_id.write({'state_id': state_id})
@ -161,6 +212,10 @@ class CarRentalContract(models.Model):
@api.constrains('checklist_line', 'damage_cost') @api.constrains('checklist_line', 'damage_cost')
def total_updater(self): def total_updater(self):
"""
Update various fields related to totals based on the values in
'checklist_line', 'damage_cost', and other relevant fields.
"""
total = 0.0 total = 0.0
tools_missing_cost = 0.0 tools_missing_cost = 0.0
for records in self.checklist_line: for records in self.checklist_line:
@ -173,11 +228,13 @@ class CarRentalContract(models.Model):
self.total_cost = tools_missing_cost + self.damage_cost self.total_cost = tools_missing_cost + self.damage_cost
def fleet_scheduler1(self, rent_date): def fleet_scheduler1(self, rent_date):
"""
Perform actions related to fleet scheduling, including creating
invoices, managing recurring data, and sending email notifications.
"""
inv_obj = self.env['account.move'] inv_obj = self.env['account.move']
inv_line_obj = self.env['account.move.line'] inv_line_obj = self.env['account.move.line']
recurring_obj = self.env['fleet.rental.line'] recurring_obj = self.env['fleet.rental.line']
start_date = datetime.strptime(str(self.rent_start_date), '%Y-%m-%d').date()
end_date = datetime.strptime(str(self.rent_end_date), '%Y-%m-%d').date()
supplier = self.customer_id supplier = self.customer_id
inv_data = { inv_data = {
'ref': supplier.name, 'ref': supplier.name,
@ -186,14 +243,16 @@ class CarRentalContract(models.Model):
'invoice_date_due': self.rent_end_date, 'invoice_date_due': self.rent_end_date,
} }
inv_id = inv_obj.create(inv_data) inv_id = inv_obj.create(inv_data)
product_id = self.env['product.product'].search([("name", "=", "Fleet Rental Service")]) product_id = self.env['product.product'].search(
[("name", "=", "Fleet Rental Service")])
if product_id.property_account_income_id.id: if product_id.property_account_income_id.id:
income_account = product_id.property_account_income_id income_account = product_id.property_account_income_id
elif product_id.categ_id.property_account_income_categ_id.id: elif product_id.categ_id.property_account_income_categ_id.id:
income_account = product_id.categ_id.property_account_income_categ_id income_account = product_id.categ_id.property_account_income_categ_id
else: else:
raise UserError( raise UserError(
_('Please define income account for this product: "%s" (id:%d).') % (product_id.name, _('Please define income account for this product: "%s" (id:%d).') % (
product_id.name,
product_id.id)) product_id.id))
recurring_data = { recurring_data = {
'name': self.vehicle_id.name, 'name': self.vehicle_id.name,
@ -225,7 +284,8 @@ class CarRentalContract(models.Model):
'<tr/><tr><td>Amount <td/><td> %s<td/><tr/>' '<tr/><tr><td>Amount <td/><td> %s<td/><tr/>'
'<tr/><tr><td>Due Date <td/><td> %s<td/><tr/>' '<tr/><tr><td>Due Date <td/><td> %s<td/><tr/>'
'<tr/><tr><td>Responsible Person <td/><td> %s, %s<td/><tr/><table/>') % \ '<tr/><tr><td>Responsible Person <td/><td> %s, %s<td/><tr/><table/>') % \
(self.customer_id.name, self.name, inv_id.amount_total, inv_id.invoice_date_due, (self.customer_id.name, self.name, inv_id.amount_total,
inv_id.invoice_date_due,
inv_id.user_id.name, inv_id.user_id.name,
inv_id.user_id.mobile) inv_id.user_id.mobile)
main_content = { main_content = {
@ -238,13 +298,19 @@ class CarRentalContract(models.Model):
@api.model @api.model
def fleet_scheduler(self): def fleet_scheduler(self):
"""
Perform fleet scheduling operations, including creating invoices,
managing recurring data, and sending email notifications.
"""
inv_obj = self.env['account.move'] inv_obj = self.env['account.move']
inv_line_obj = self.env['account.move.line'] inv_line_obj = self.env['account.move.line']
recurring_obj = self.env['fleet.rental.line'] recurring_obj = self.env['fleet.rental.line']
today = date.today() today = date.today()
for records in self.search([]): for records in self.search([]):
start_date = datetime.strptime(str(records.rent_start_date), '%Y-%m-%d').date() start_date = datetime.strptime(str(records.rent_start_date),
end_date = datetime.strptime(str(records.rent_end_date), '%Y-%m-%d').date() '%Y-%m-%d').date()
end_date = datetime.strptime(str(records.rent_end_date),
'%Y-%m-%d').date()
if end_date >= date.today(): if end_date >= date.today():
temp = 0 temp = 0
if records.cost_frequency == 'daily': if records.cost_frequency == 'daily':
@ -268,18 +334,21 @@ class CarRentalContract(models.Model):
'currency_id': records.account_type.company_id.currency_id.id, 'currency_id': records.account_type.company_id.currency_id.id,
'journal_id': records.journal_type.id, 'journal_id': records.journal_type.id,
'invoice_origin': records.name, 'invoice_origin': records.name,
'fleet_rent_id': records.id,
'company_id': records.account_type.company_id.id, 'company_id': records.account_type.company_id.id,
'invoice_date_due': self.rent_end_date, 'invoice_date_due': self.rent_end_date,
} }
inv_id = inv_obj.create(inv_data) inv_id = inv_obj.create(inv_data)
product_id = self.env['product.product'].search([("name", "=", "Fleet Rental Service")]) product_id = self.env['product.product'].search(
[("name", "=", "Fleet Rental Service")])
if product_id.property_account_income_id.id: if product_id.property_account_income_id.id:
income_account = product_id.property_account_income_id income_account = product_id.property_account_income_id
elif product_id.categ_id.property_account_income_categ_id.id: elif product_id.categ_id.property_account_income_categ_id.id:
income_account = product_id.categ_id.property_account_income_categ_id income_account = product_id.categ_id.property_account_income_categ_id
else: else:
raise UserError( raise UserError(
_('Please define income account for this product: "%s" (id:%d).') % (product_id.name, _('Please define income account for this product: "%s" (id:%d).') % (
product_id.name,
product_id.id)) product_id.id))
recurring_data = { recurring_data = {
'name': records.vehicle_id.name, 'name': records.vehicle_id.name,
@ -312,7 +381,9 @@ class CarRentalContract(models.Model):
'<tr/><tr><td>Amount <td/><td> %s<td/><tr/>' '<tr/><tr><td>Amount <td/><td> %s<td/><tr/>'
'<tr/><tr><td>Due Date <td/><td> %s<td/><tr/>' '<tr/><tr><td>Due Date <td/><td> %s<td/><tr/>'
'<tr/><tr><td>Responsible Person <td/><td> %s, %s<td/><tr/><table/>') % \ '<tr/><tr><td>Responsible Person <td/><td> %s, %s<td/><tr/><table/>') % \
(self.customer_id.name, self.name, inv_id.amount_total, inv_id.invoice_date_due, (self.customer_id.name, self.name,
inv_id.amount_total,
inv_id.invoice_date_due,
inv_id.user_id.name, inv_id.user_id.name,
inv_id.user_id.mobile) inv_id.user_id.mobile)
main_content = { main_content = {
@ -330,40 +401,39 @@ class CarRentalContract(models.Model):
self.state = "invoice" self.state = "invoice"
self.reserved_fleet_id.unlink() self.reserved_fleet_id.unlink()
self.rent_end_date = fields.Date.today() self.rent_end_date = fields.Date.today()
self.vehicle_id.rental_check_availability = True
product_id = self.env['product.product'].search(
[("name", "=", "Fleet Rental Service")])
if product_id.property_account_income_id.id:
income_account = product_id.property_account_income_id
elif product_id.categ_id.property_account_income_categ_id.id:
income_account = product_id.categ_id.property_account_income_categ_id
else:
raise UserError(
_('Please define income account for this product: "%s" (id:%d).') % (
product_id.name,
product_id.id))
if self.total_cost != 0: if self.total_cost != 0:
inv_obj = self.env['account.move']
inv_line_obj = self.env['account.move.line']
supplier = self.customer_id supplier = self.customer_id
inv_data = { inv_data = {
'ref': supplier.name, 'ref': supplier.name,
'move_type': 'out_invoice',
'partner_id': supplier.id, 'partner_id': supplier.id,
'currency_id': self.account_type.company_id.currency_id.id, 'currency_id': self.account_type.company_id.currency_id.id,
'journal_id': self.journal_type.id, 'journal_id': self.journal_type.id,
'invoice_origin': self.name, 'invoice_origin': self.name,
'fleet_rent_id': self.id,
'company_id': self.account_type.company_id.id, 'company_id': self.account_type.company_id.id,
'invoice_date_due': self.rent_end_date, 'invoice_date_due': self.rent_end_date,
} 'invoice_line_ids': [(0, 0, {
inv_id = inv_obj.create(inv_data)
product_id = self.env['product.product'].search([("name", "=", "Fleet Rental Service")])
if product_id.property_account_income_id.id:
income_account = product_id.property_account_income_id
elif product_id.categ_id.property_account_income_categ_id.id:
income_account = product_id.categ_id.property_account_income_categ_id
else:
raise UserError(
_('Please define income account for this product: "%s" (id:%d).') % (product_id.name,
product_id.id))
inv_line_data = {
'name': "Damage/Tools missing cost", 'name': "Damage/Tools missing cost",
'account_id': income_account.id, 'account_id': income_account.id,
'price_unit': self.total_cost, 'price_unit': self.total_cost,
'quantity': 1, 'quantity': 1,
'product_id': product_id.id, 'product_id': product_id.id,
'move_id': inv_id.id, })]
} }
inv_line_obj.create(inv_line_data) inv_id = self.env['account.move'].create(inv_data)
imd = self.env['ir.model.data']
action = self.env.ref('account.view_move_tree')
list_view_id = self.env.ref('account.view_move_form', False) list_view_id = self.env.ref('account.view_move_form', False)
form_view_id = self.env.ref('account.view_move_tree', False) form_view_id = self.env.ref('account.view_move_tree', False)
result = { result = {
@ -371,9 +441,9 @@ class CarRentalContract(models.Model):
'view_mode': 'form', 'view_mode': 'form',
'res_model': 'account.move', 'res_model': 'account.move',
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'views': [(list_view_id.id, 'tree'), (form_view_id.id, 'form')], 'views': [(list_view_id.id, 'tree'),
(form_view_id.id, 'form')],
} }
if len(inv_id) > 1: if len(inv_id) > 1:
result['domain'] = "[('id','in',%s)]" % inv_id.ids result['domain'] = "[('id','in',%s)]" % inv_id.ids
else: else:
@ -381,8 +451,12 @@ class CarRentalContract(models.Model):
return result return result
def action_confirm(self): def action_confirm(self):
if self.rent_end_date < self.rent_start_date: """
raise ValidationError("Please select the valid end date.") Confirm the rental contract, check vehicle availability, update
state to "reserved," generate a sequence code, and send a
confirmation email.
"""
self.vehicle_id.rental_check_availability = False
check_availability = 0 check_availability = 0
for each in self.vehicle_id.rental_reserved_time: for each in self.vehicle_id.rental_reserved_time:
if each.date_from <= self.rent_start_date <= each.date_to: if each.date_from <= self.rent_start_date <= each.date_to:
@ -397,28 +471,34 @@ class CarRentalContract(models.Model):
else: else:
check_availability = 0 check_availability = 0
if check_availability == 0: if check_availability == 0:
reserved_id = self.vehicle_id.rental_reserved_time.create({'customer_id': self.customer_id.id, reserved_id = self.vehicle_id.rental_reserved_time.create(
{'customer_id': self.customer_id.id,
'date_from': self.rent_start_date, 'date_from': self.rent_start_date,
'date_to': self.rent_end_date, 'date_to': self.rent_end_date,
'reserved_obj': self.vehicle_id.id 'reserved_obj': self.vehicle_id.id
}) })
self.write({'reserved_fleet_id': reserved_id.id}) self.write({'reserved_fleet_id': reserved_id.id})
else: else:
raise Warning('Sorry This vehicle is already booked by another customer') raise UserError(
'Sorry This vehicle is already booked by another customer')
self.state = "reserved" self.state = "reserved"
sequence_code = 'car.rental.sequence' sequence_code = 'car.rental.sequence'
order_date = self.create_date order_date = self.create_date
order_date = str(order_date)[0:10] order_date = str(order_date)[0:10]
self.name = self.env['ir.sequence'] \ self.name = self.env['ir.sequence'] \
.with_context(ir_sequence_date=order_date).next_by_code(sequence_code) .with_context(ir_sequence_date=order_date).next_by_code(
mail_content = _('<h3>Order Confirmed!</h3><br/>Hi %s, <br/> This is to notify that your rental contract has ' sequence_code)
mail_content = _(
'<h3>Order Confirmed!</h3><br/>Hi %s, <br/> This is to notify that your rental contract has '
'been confirmed. <br/><br/>' 'been confirmed. <br/><br/>'
'Please find the details below:<br/><br/>' 'Please find the details below:<br/><br/>'
'<table><tr><td>Reference Number<td/><td> %s<td/><tr/>' '<table><tr><td>Reference Number<td/><td> %s<td/><tr/>'
'<tr><td>Time Range <td/><td> %s to %s <td/><tr/><tr><td>Vehicle <td/><td> %s<td/><tr/>' '<tr><td>Time Range <td/><td> %s to %s <td/><tr/><tr><td>Vehicle <td/><td> %s<td/><tr/>'
'<tr><td>Point Of Contact<td/><td> %s , %s<td/><tr/><table/>') % \ '<tr><td>Point Of Contact<td/><td> %s , %s<td/><tr/><table/>') % \
(self.customer_id.name, self.name, self.rent_start_date, self.rent_end_date, (self.customer_id.name, self.name, self.rent_start_date,
self.vehicle_id.name, self.sales_person.name, self.sales_person.mobile) self.rent_end_date,
self.vehicle_id.name, self.sales_person.name,
self.sales_person.mobile)
main_content = { main_content = {
'subject': _('Confirmed: %s - %s') % 'subject': _('Confirmed: %s - %s') %
(self.name, self.vehicle_id.name), (self.name, self.vehicle_id.name),
@ -429,15 +509,39 @@ class CarRentalContract(models.Model):
self.env['mail.mail'].create(main_content).send() self.env['mail.mail'].create(main_content).send()
def action_cancel(self): def action_cancel(self):
"""
Cancel the rental contract.
Update the state to "cancel" and delete the associated reserved
fleet ID if it exists.
"""
self.state = "cancel" self.state = "cancel"
self.vehicle_id.rental_check_availability = True
if self.reserved_fleet_id: if self.reserved_fleet_id:
self.reserved_fleet_id.unlink() self.reserved_fleet_id.unlink()
def force_checking(self): def force_checking(self):
"""
Force the checking of payment status for associated invoices.
If all invoices are marked as paid, update the state to "checking."
Otherwise, raise a UserError indicating that some invoices are
pending.
"""
invoice_ids = self.env['account.move'].search(
[('fleet_rent_id', '=', self.id),
('amount_untaxed_signed', '=', self.first_payment)])
if any(each.payment_state == 'paid' for each in invoice_ids):
self.state = "checking" self.state = "checking"
else:
raise UserError("Some Invoices are pending")
def action_view_invoice(self): def action_view_invoice(self):
inv_obj = self.env['account.move'].search([('invoice_origin', '=', self.name)]) """
Display the associated invoices for the current record.
Construct the appropriate view configurations based on the number
of invoices found.
"""
inv_obj = self.env['account.move'].search(
[('fleet_rent_id', '=', self.id)])
inv_ids = [] inv_ids = []
for each in inv_obj: for each in inv_obj:
inv_ids.append(each.id) inv_ids.append(each.id)
@ -455,7 +559,7 @@ class CarRentalContract(models.Model):
} }
else: else:
value = { value = {
'domain': str([('id', 'in', inv_ids)]), 'domain': [('fleet_rent_id', '=', self.id)],
'view_type': 'form', 'view_type': 'form',
'view_mode': 'tree,form', 'view_mode': 'tree,form',
'res_model': 'account.move', 'res_model': 'account.move',
@ -463,10 +567,16 @@ class CarRentalContract(models.Model):
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': _('Invoice'), 'name': _('Invoice'),
} }
return value return value
def action_invoice_create(self): def action_invoice_create(self):
"""
Create an invoice for the rental contract.
Calculate the rental duration and iterate over each day to create
invoices.
Create the first payment invoice, add relevant invoice line data,
and send an email notification for the received payment.
"""
for each in self: for each in self:
rent_date = self.rent_start_date rent_date = self.rent_start_date
if each.cost_frequency != 'no' and rent_date < date.today(): if each.cost_frequency != 'no' and rent_date < date.today():
@ -476,7 +586,8 @@ class CarRentalContract(models.Model):
if each.cost_frequency == 'monthly': if each.cost_frequency == 'monthly':
rental_days = int(rental_days / 30) rental_days = int(rental_days / 30)
for each1 in range(0, rental_days + 1): for each1 in range(0, rental_days + 1):
if rent_date > datetime.strptime(str(each.rent_end_date), "%Y-%m-%d").date(): if rent_date > datetime.strptime(str(each.rent_end_date),
"%Y-%m-%d").date():
break break
each.fleet_scheduler1(rent_date) each.fleet_scheduler1(rent_date)
if each.cost_frequency == 'daily': if each.cost_frequency == 'daily':
@ -485,11 +596,8 @@ class CarRentalContract(models.Model):
rent_date = rent_date + timedelta(days=7) rent_date = rent_date + timedelta(days=7)
if each.cost_frequency == 'monthly': if each.cost_frequency == 'monthly':
rent_date = rent_date + timedelta(days=30) rent_date = rent_date + timedelta(days=30)
if self.first_payment != 0:
self.first_invoice_created = True self.first_invoice_created = True
inv_obj = self.env['account.move'] inv_obj = self.env['account.move']
inv_line_obj = self.env['account.move.line']
supplier = self.customer_id supplier = self.customer_id
inv_data = { inv_data = {
'ref': supplier.name, 'ref': supplier.name,
@ -498,19 +606,22 @@ class CarRentalContract(models.Model):
'currency_id': self.account_type.company_id.currency_id.id, 'currency_id': self.account_type.company_id.currency_id.id,
'journal_id': self.journal_type.id, 'journal_id': self.journal_type.id,
'invoice_origin': self.name, 'invoice_origin': self.name,
'fleet_rent_id': self.id,
'company_id': self.account_type.company_id.id, 'company_id': self.account_type.company_id.id,
'invoice_date_due': self.rent_end_date, 'invoice_date_due': self.rent_end_date,
} }
inv_id = inv_obj.create(inv_data) inv_id = inv_obj.create(inv_data)
self.first_payment_inv = inv_id.id self.first_payment_inv = inv_id.id
product_id = self.env['product.product'].search([("name", "=", "Fleet Rental Service")]) product_id = self.env['product.product'].search(
[("name", "=", "Fleet Rental Service")])
if product_id.property_account_income_id.id: if product_id.property_account_income_id.id:
income_account = product_id.property_account_income_id.id income_account = product_id.property_account_income_id.id
elif product_id.categ_id.property_account_income_categ_id.id: elif product_id.categ_id.property_account_income_categ_id.id:
income_account = product_id.categ_id.property_account_income_categ_id.id income_account = product_id.categ_id.property_account_income_categ_id.id
else: else:
raise UserError( raise UserError(
_('Please define income account for this product: "%s" (id:%d).') % (product_id.name, _('Please define income account for this product: "%s" (id:%d).') % (
product_id.name,
product_id.id)) product_id.id))
if inv_id: if inv_id:
@ -529,7 +640,8 @@ class CarRentalContract(models.Model):
'Please find the details below:<br/><br/>' 'Please find the details below:<br/><br/>'
'<table><tr><td>Invoice Number<td/><td> %s<td/><tr/>' '<table><tr><td>Invoice Number<td/><td> %s<td/><tr/>'
'<tr><td>Date<td/><td> %s <td/><tr/><tr><td>Amount <td/><td> %s<td/><tr/><table/>') % ( '<tr><td>Date<td/><td> %s <td/><tr/><tr><td>Amount <td/><td> %s<td/><tr/><table/>') % (
self.customer_id.name, inv_id.payment_reference, inv_id.invoice_date, inv_id.amount_total) self.customer_id.name, inv_id.payment_reference,
inv_id.invoice_date, inv_id.amount_total)
main_content = { main_content = {
'subject': _('Payment Received: %s') % inv_id.payment_reference, 'subject': _('Payment Received: %s') % inv_id.payment_reference,
'author_id': self.env.user.partner_id.id, 'author_id': self.env.user.partner_id.id,
@ -537,7 +649,6 @@ class CarRentalContract(models.Model):
'email_to': self.customer_id.email, 'email_to': self.customer_id.email,
} }
self.env['mail.mail'].create(main_content).send() self.env['mail.mail'].create(main_content).send()
imd = self.env['ir.model.data']
action = self.env.ref('account.action_move_out_invoice_type') action = self.env.ref('account.action_move_out_invoice_type')
result = { result = {
'name': action.name, 'name': action.name,
@ -549,46 +660,83 @@ class CarRentalContract(models.Model):
} }
return result return result
else: def action_extend_rent(self):
raise ValidationError("Please enter advance amount to make first payment") """
Set the 'read_only' attribute to True, indicating that the rent
extension action is being performed and the corresponding fields
class FleetRentalLine(models.Model): should be made read-only.
_name = 'fleet.rental.line'
This method is typically used in the context of extending a rental
name = fields.Char('Description') agreement.
date_today = fields.Date('Date') """
account_info = fields.Char('Account') self.read_only = True
recurring_amount = fields.Float('Amount')
rental_number = fields.Many2one('car.rental.contract', string='Rental Number') def action_confirm_extend_rent(self):
payment_info = fields.Char(compute='paid_info', string='Payment Stage', default='draft') """
invoice_number = fields.Integer(string='Invoice ID') Confirm the extension of a rental agreement and update the rental
invoice_ref = fields.Many2one('account.move', string='Invoice Ref') reserved time for the associated vehicle.
date_due = fields.Date(string='Due Date', related='invoice_ref.invoice_date_due')
This method sets the 'date_to' field of the rental reserved time
def paid_info(self): for the vehicle to the specified 'rent_end_date', indicating the
for each in self: extended rental period. After confirming the extension, the
if self.env['account.move'].browse(each.invoice_number): 'read_only' attribute is set to False to allow further
each.payment_info = self.env['account.move'].browse(each.invoice_number).state modifications.
else:
each.payment_info = 'Record Deleted' This method is typically called when a user confirms the extension
of a rental.
"""
class CarRentalChecklist(models.Model): self.vehicle_id.rental_reserved_time.write(
_name = 'car.rental.checklist' {
'date_to': self.rent_end_date,
name = fields.Many2one('car.tools', string="Name") })
checklist_active = fields.Boolean(string="Available", default=True) self.read_only = False
checklist_number = fields.Many2one('car.rental.contract', string="Checklist Number")
price = fields.Float(string="Price") @api.constrains('start_time', 'end_time', 'rent_start_date',
'rent_end_date')
@api.onchange('name') def validate_time(self):
def onchange_name(self): """
self.price = self.name.price Validate the time constraints for a rental agreement.
This method is used as a constraint to ensure that the specified
class CarTools(models.Model): start and end times are valid, especially when renting by the hour.
_name = 'car.tools' If renting by the hour, it checks whether the end time is greater
than the start time when the rental start and end
name = fields.Char(string="Name") dates are the same.
price = fields.Float(string="Price")
:raises ValidationError: If the time constraints are not met, a
validation error is raised with a relevant
error message.
"""
if self.rent_by_hour:
start_time = datetime.strptime(self.start_time, "%H:%M").time()
end_time = datetime.strptime(self.end_time, "%H:%M").time()
if (self.rent_end_date == self.rent_start_date and
end_time <= start_time):
raise ValidationError("Please choose a different end time")
@api.constrains('rent_end_date')
def validate_on_read_only(self):
old_date = self.vehicle_id.rental_reserved_time.date_to
if self.read_only:
if self.rent_end_date <= old_date:
raise ValidationError(
f"Please choose a date greater that {old_date}")
def action_discard_extend(self):
"""
Validate the 'rent_end_date' when the rental agreement is in
read-only mode.
This constraint checks if the rental agreement is marked as
read-only, indicating that it has been extended or modified. If in
read-only mode, it compares the 'rent_end_date' with the existing
'date_to' value in the rental reserved time of the associated
vehicle. It ensures that the 'rent_end_date' is greater than the
existing date to maintain consistency.
:raises ValidationError: If the 'rent_end_date' is not greater than
the existing 'date_to', a validation error
is raised with a relevant error message.
"""
self.read_only = False
self.rent_end_date = self.vehicle_id.rental_reserved_time.date_to

40
fleet_rental/models/car_rental_checklist.py

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import api, fields, models
class CarRentalChecklist(models.Model):
"""Model to add the checklist of rental"""
_name = 'car.rental.checklist'
name = fields.Many2one('car.tools', string="Name")
checklist_active = fields.Boolean(string="Available", default=True)
checklist_number = fields.Many2one('car.rental.contract',
string="Checklist Number")
price = fields.Float(string="Price")
@api.onchange('name')
def onchange_name(self):
"""
Update the price based on the selected name.
"""
self.price = self.name.price

30
fleet_rental/models/car_tools.py

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class CarTools(models.Model):
"""Model to add the tools for the car"""
_name = 'car.tools'
name = fields.Char(string="Name")
price = fields.Float(string="Price")

21
fleet_rental/models/fleet.py

@ -3,7 +3,7 @@
# #
# Cybrosys Technologies Pvt. Ltd. # Cybrosys Technologies Pvt. Ltd.
# #
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). # Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com) # Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
# #
# You can modify it under the terms of the GNU AFFERO # You can modify it under the terms of the GNU AFFERO
@ -19,7 +19,6 @@
# If not, see <http://www.gnu.org/licenses/>. # If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################# #############################################################################
from odoo import models, fields from odoo import models, fields
@ -31,21 +30,3 @@ class FleetReservedTime(models.Model):
date_from = fields.Date(string='Reserved Date From') date_from = fields.Date(string='Reserved Date From')
date_to = fields.Date(string='Reserved Date To') date_to = fields.Date(string='Reserved Date To')
reserved_obj = fields.Many2one('fleet.vehicle') reserved_obj = fields.Many2one('fleet.vehicle')
class EmployeeFleet(models.Model):
_inherit = 'fleet.vehicle'
rental_check_availability = fields.Boolean(default=True, copy=False)
color = fields.Char(string='Color', default='#FFFFFF')
rental_reserved_time = fields.One2many('rental.fleet.reserved', 'reserved_obj', string='Reserved Time', readonly=1,
ondelete='cascade')
fuel_type = fields.Selection([('gasoline', 'Gasoline'),
('diesel', 'Diesel'),
('electric', 'Electric'),
('hybrid', 'Hybrid'),
('petrol', 'Petrol')],
'Fuel Type', help='Fuel Used by the vehicle')
_sql_constraints = [('vin_sn_unique', 'unique (vin_sn)', "Chassis Number already exists !"),
('license_plate_unique', 'unique (license_plate)', "License plate already exists !")]

55
fleet_rental/models/fleet_rental_line.py

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class FleetRentalLine(models.Model):
_name = 'fleet.rental.line'
name = fields.Char('Description')
date_today = fields.Date('Date')
account_info = fields.Char('Account')
recurring_amount = fields.Float('Amount')
rental_number = fields.Many2one('car.rental.contract',
string='Rental Number')
payment_info = fields.Char(compute='paid_info', string='Payment Stage',
default='draft')
invoice_number = fields.Integer(string='Invoice ID')
invoice_ref = fields.Many2one('account.move', string='Invoice Ref')
date_due = fields.Date(string='Due Date',
related='invoice_ref.invoice_date_due')
def paid_info(self):
"""
Retrieve payment information for the current record.
Check the state of the associated invoice based on the provided
invoice number.
If the record exists, set the payment_info field to the state of
the invoice.
Otherwise, set the payment_info field to 'Record Deleted'.
"""
for each in self:
if self.env['account.move'].browse(each.invoice_number):
each.payment_info = self.env['account.move'].browse(
each.invoice_number).state
else:
each.payment_info = 'Record Deleted'

45
fleet_rental/models/fleet_vehicle.py

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class EmployeeFleet(models.Model):
"""Inherit fleet.vehicle"""
_inherit = 'fleet.vehicle'
rental_check_availability = fields.Boolean(default=True, copy=False)
color = fields.Char(string='Color', default='#FFFFFF')
rental_reserved_time = fields.One2many('rental.fleet.reserved',
'reserved_obj',
string='Reserved Time', readonly=1,
ondelete='cascade')
fuel_type = fields.Selection([('gasoline', 'Gasoline'),
('diesel', 'Diesel'),
('electric', 'Electric'),
('hybrid', 'Hybrid'),
('petrol', 'Petrol')],
'Fuel Type', help='Fuel Used by the vehicle')
_sql_constraints = [('vin_sn_unique', 'unique (vin_sn)',
"Chassis Number already exists !"),
('license_plate_unique', 'unique (license_plate)',
"License plate already exists !")]

39
fleet_rental/models/res_config_settings.py

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
"""Inherit configuration settings"""
_inherit = 'res.config.settings'
def _get_default_product(self):
"""
Retrieve the default product ID for fleet services.
"""
return self.env.ref('fleet_rental.fleet_service_product').id
fleet_service_product_id = fields.Many2one(
'product.template',
string="Product",
config_parameter='fleet_service_product_id',
default=_get_default_product)

2
fleet_rental/reports/__init__.py

@ -3,7 +3,7 @@
# #
# Cybrosys Technologies Pvt. Ltd. # Cybrosys Technologies Pvt. Ltd.
# #
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). # Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com) # Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
# #
# You can modify it under the terms of the GNU AFFERO # You can modify it under the terms of the GNU AFFERO

21
fleet_rental/reports/rental_report.py

@ -3,7 +3,7 @@
# #
# Cybrosys Technologies Pvt. Ltd. # Cybrosys Technologies Pvt. Ltd.
# #
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). # Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com) # Author: Cybrosys Technogies @cybrosys(odoo@cybrosys.com)
# #
# You can modify it under the terms of the GNU AFFERO # You can modify it under the terms of the GNU AFFERO
@ -36,9 +36,12 @@ class FleetRentalReport(models.Model):
cost = fields.Float(string="Rent Cost") cost = fields.Float(string="Rent Cost")
rent_start_date = fields.Date(string="Rent Start Date") rent_start_date = fields.Date(string="Rent Start Date")
rent_end_date = fields.Date(string="Rent End Date") rent_end_date = fields.Date(string="Rent End Date")
state = fields.Selection([('draft', 'Draft'), ('running', 'Running'), ('cancel', 'Cancel'), state = fields.Selection(
[('draft', 'Draft'), ('running', 'Running'), ('cancel', 'Cancel'),
('checking', 'Checking'), ('done', 'Done')], string="State") ('checking', 'Checking'), ('done', 'Done')], string="State")
cost_frequency = fields.Selection([('no', 'No'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), cost_frequency = fields.Selection(
[('no', 'No'), ('daily', 'Daily'), ('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly')], string="Recurring Cost Frequency") ('yearly', 'Yearly')], string="Recurring Cost Frequency")
total = fields.Float(string="Total(Tools)") total = fields.Float(string="Total(Tools)")
tools_missing_cost = fields.Float(string="Tools missing cost") tools_missing_cost = fields.Float(string="Tools missing cost")
@ -49,6 +52,9 @@ class FleetRentalReport(models.Model):
_order = 'name desc' _order = 'name desc'
def _select(self): def _select(self):
"""
Construct a SQL select query string with specific fields.
"""
select_str = """ select_str = """
SELECT SELECT
(select 1 ) AS nbr, (select 1 ) AS nbr,
@ -72,6 +78,9 @@ class FleetRentalReport(models.Model):
return select_str return select_str
def _group_by(self): def _group_by(self):
"""
Construct a SQL GROUP BY query string with specific fields.
"""
group_by_str = """ group_by_str = """
GROUP BY GROUP BY
t.id, t.id,
@ -94,6 +103,12 @@ class FleetRentalReport(models.Model):
return group_by_str return group_by_str
def init(self): def init(self):
"""
Initialize the module and create a database view for reporting
fleet rentals.
Drop the existing 'report_fleet_rental' view if it already exists.
Create a new view with the SQL select and group by queries.
"""
tools.sql.drop_view_if_exists(self._cr, 'report_fleet_rental') tools.sql.drop_view_if_exists(self._cr, 'report_fleet_rental')
self._cr.execute(""" self._cr.execute("""
CREATE view report_fleet_rental as CREATE view report_fleet_rental as

14
fleet_rental/reports/rental_report.xml

@ -1,26 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<data> <data>
<record id="view_report_car_rental" model="ir.ui.view"> <record id="view_report_car_rental" model="ir.ui.view">
<field name="name">report.fleet.rental.pivot</field> <field name="name">report.fleet.rental.pivot</field>
<field name="model">report.fleet.rental</field> <field name="model">report.fleet.rental</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<pivot string="Fleet Rental Analysis" display_quantity="true" disable_linking="True"> <pivot string="Fleet Rental Analysis" display_quantity="true"
disable_linking="True">
<field name="name" type="row"/> <field name="name" type="row"/>
</pivot> </pivot>
</field> </field>
</record> </record>
<record id="action_fleet_rental_analysis" model="ir.actions.act_window"> <record id="action_fleet_rental_analysis" model="ir.actions.act_window">
<field name="name">Fleet Rental Analysis</field> <field name="name">Fleet Rental Analysis</field>
<field name="res_model">report.fleet.rental</field> <field name="res_model">report.fleet.rental</field>
<field name="view_mode">pivot</field> <field name="view_mode">pivot</field>
<field name="context">{'group_by_no_leaf':1,'group_by':[]}</field> <field name="context">{'group_by_no_leaf':1,'group_by':[]}</field>
<field name="help">This report allows you to analyse the performance of your Fleet Rental. </field> <field name="help">This report allows you to analyse the performance of your
Fleet Rental.
</field>
</record> </record>
<menuitem name="Fleet Rental Analysis" action="action_fleet_rental_analysis"
<menuitem name="Fleet Rental Analysis" action="action_fleet_rental_analysis" id="menu_fleet_rental_analysis" parent="fleet.menu_fleet_reporting" sequence="1"/> id="menu_fleet_rental_analysis" parent="fleet.menu_fleet_reporting"
sequence="1"/>
</data> </data>
</odoo> </odoo>

32
fleet_rental/static/src/js/time_widget.js

@ -0,0 +1,32 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useInputField } from "@web/views/fields/input_field_hook";
const { Component, useRef } = owl;
/**
* We define this module for the function of creating a time picker widget
*
*/
class FieldTimePicker extends Component {
setup() {
this.input = useRef('input_time');
useInputField({ getValue: () => this.props.record.data[this.props.name] || "", refName: "input_time" });
}
onBlur(){
/**
* Handle the blur event for the timepicker input field.
*
* This function is responsible for handling the blur event on the timepicker input field.
* It checks if the close button is present in the timepicker, and if so, it adds a click event
* listener to it to handle the closing of the timepicker.
*
* @returns {void}
*/
this.props.record.update({ [this.props.name] : this.input.el?.value})
}
}
// Set the template for the FieldTimePicker component
FieldTimePicker.template = 'FieldTimePicker';
FieldTimePicker.supportedTypes = ["char"]
// Add the timepicker to the fields category
registry.category("fields").add("timepicker_time", FieldTimePicker);

3
fleet_rental/static/src/scss/timepicker.scss

@ -0,0 +1,3 @@
.timepicker-component{
width:30%;
}

9
fleet_rental/static/src/xml/timepicker.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="FieldTimePicker" owl="1">
<!-- FieldTimePicker template to add input field-->
<div class="input-group timepicker-component">
<input type="time" class="time-input" t-ref="input_time" t-on-blur="onBlur"/>
</div>
</t>
</templates>

91
fleet_rental/views/car_rental_view.xml

@ -49,25 +49,42 @@
<form string="car_result"> <form string="car_result">
<header> <header>
<button name="action_confirm" string="Confirm" type="object" <button name="action_confirm" string="Confirm" type="object"
attrs="{'invisible': [('state', '!=', 'draft')]}" class="oe_highlight"/> attrs="{'invisible': [('state', '!=', 'draft')]}"
class="oe_highlight"/>
<button name="action_run" string="Run" type="object" <button name="action_run" string="Run" type="object"
attrs="{'invisible': [('state', '!=', 'reserved')]}" class="oe_highlight"/> attrs="{'invisible': [('state', '!=', 'reserved')]}"
class="oe_highlight"/>
<button name="action_cancel" string="Cancel" type="object" <button name="action_cancel" string="Cancel" type="object"
attrs="{'invisible': [('state', 'not in', ('draft', 'reserved'))]}" class="oe_highlight"/> attrs="{'invisible': [('state', 'not in', ('draft', 'reserved'))]}"
class="oe_highlight"/>
<button name="action_invoice_create" string="Create Invoice" <button name="action_invoice_create" string="Create Invoice"
attrs="{'invisible': ['|',('state', '!=', 'running'), attrs="{'invisible': ['|','|',('state', '!=', 'running'),
('first_invoice_created','=',True)]}" type="object" class="oe_highlight"/> ('first_invoice_created','=',True),('read_only','=',True)]}" type="object"
class="oe_highlight"/>
<button name="action_extend_rent" string="Extend Rent"
attrs="{'invisible': ['|','|',('state', '!=', 'running'),('read_only','=',True),('first_invoice_created','=',False)]}" type="object"
class="oe_highlight"/>
<button name="action_confirm_extend_rent" string="Confirm Extend"
attrs="{'invisible': ['|',('state', '!=', 'running'),('read_only','=',False)]}" type="object"
class="oe_highlight"/>
<button name="action_discard_extend" string="Discard Extend"
attrs="{'invisible': ['|',('state', '!=', 'running'),('read_only','=',False)]}" type="object"
class="oe_highlight"/>
<button name="force_checking" string="Force Checking" <button name="force_checking" string="Force Checking"
attrs="{'invisible': ['|',('state','!=','running'), attrs="{'invisible': ['|',('state','!=','running'),
('first_invoice_created','=',False)]}" type="object" class="oe_highlight"/> ('first_invoice_created','=',False)]}" type="object"
class="oe_highlight"/>
<button name="set_to_done" string="Set to Done" <button name="set_to_done" string="Set to Done"
states="invoice" type="object" class="oe_highlight"/> states="invoice" type="object" class="oe_highlight"/>
<field name="state" widget="statusbar" statusbar_visible="draft,running,done"/> <field name="state" widget="statusbar"
statusbar_visible="draft,running,done"/>
</header> </header>
<sheet> <sheet>
<div class="oe_button_box" name="buttons"> <div class="oe_button_box" name="buttons">
<button name='action_view_invoice' class="oe_stat_button" type="object" icon="fa-money"> <button name='action_view_invoice' class="oe_stat_button"
<field string="Invoice" name="invoice_count" widget="statinfo" /> type="object" icon="fa-money">
<field string="Invoice" name="invoice_count"
widget="statinfo"/>
</button> </button>
</div> </div>
<field name="image" widget='image' class="oe_avatar"/> <field name="image" widget='image' class="oe_avatar"/>
@ -77,17 +94,27 @@
</h1> </h1>
</div> </div>
<group> <group>
<separator string="Contract Details " colspan="4" /> <separator string="Contract Details " colspan="4"/>
<group> <group>
<field name="customer_id" string="Customer" attrs="{'readonly': [('state','!=','draft')]}"/> <field name="customer_id" string="Customer"
<field name="rent_start_date" attrs="{'readonly': [('state','!=','draft')]}"/> attrs="{'readonly': [('state','!=','draft')]}"/>
<field name="rent_end_date" attrs="{'readonly': [('state','!=','draft')]}"/> <field name="rent_by_hour" attrs="{'readonly': [('state','!=','draft')]}"/>
<field name="read_only" invisible="1"/>
<field name="rent_start_date"
attrs="{'readonly': [('state','!=','draft')]}"/>
<field name="start_time" widget="timepicker_time" attrs="{'invisible': [('rent_by_hour','=',False)],
'readonly': [('state','!=','draft')],'required':[('rent_by_hour','=',True)]}"/>
<field name="rent_end_date"
attrs="{'readonly': [('state','!=','draft'),('read_only','=',False)]}"/>
<field name="end_time" widget="timepicker_time" attrs="{'invisible': [('rent_by_hour','=',False)],
'readonly': [('state','!=','draft'),('read_only','=',False)],'required':[('rent_by_hour','=',True)]}"/>
<field name="vehicle_id" domain="[('rental_check_availability','=',True), <field name="vehicle_id" domain="[('rental_check_availability','=',True),
('state_id.name','!=','Inactive')]" ('state_id.name','!=','Inactive')]"
options="{'no_create': True}"/> options="{'no_create': True}"/>
<field name="journal_type" invisible="1"/> <field name="journal_type" invisible="1"/>
<field name="check_verify" invisible="1" /> <field name="check_verify" invisible="1"/>
<field name="sales_person" attrs="{'readonly': [('state','!=','draft')]}"/> <field name="sales_person"
attrs="{'readonly': [('state','!=','draft')]}"/>
</group> </group>
<group> <group>
<field name="car_brand"/> <field name="car_brand"/>
@ -114,7 +141,8 @@
</group> </group>
</group> </group>
<notebook> <notebook>
<page string="Recurring Invoices" attrs="{'invisible': [('cost_frequency','in',[None,False,'no'])]}"> <page string="Recurring Invoices"
attrs="{'invisible': [('cost_frequency','in',[None,False,'no'])]}">
<field name="recurring_line" mode="tree"> <field name="recurring_line" mode="tree">
<tree string="Fleet Reccurring Lines" <tree string="Fleet Reccurring Lines"
colors="#0b7a35:payment_info=='paid';#f20b07:payment_info!='paid'"> colors="#0b7a35:payment_info=='paid';#f20b07:payment_info!='paid'">
@ -131,14 +159,18 @@
<page string="Checklist"> <page string="Checklist">
<group> <group>
<group> <group>
<field name="attachment_ids" widget="many2many_binary" class="oe_inline"/> <field name="attachment_ids"
widget="many2many_binary"
class="oe_inline"/>
</group> </group>
<group> <group>
<field name="damage_cost" attrs="{'invisible': [('state','!=','checking')]}"/> <field name="damage_cost"
attrs="{'invisible': [('state','!=','checking')]}"/>
</group> </group>
</group> </group>
<field name="checklist_line"> <field name="checklist_line">
<tree string="Fleet Checklist Lines" editable="bottom"> <tree string="Fleet Checklist Lines"
editable="bottom">
<field name="name"/> <field name="name"/>
<field name="checklist_active"/> <field name="checklist_active"/>
<field name="checklist_number" invisible="True"/> <field name="checklist_number" invisible="True"/>
@ -148,8 +180,10 @@
<sheet> <sheet>
<group> <group>
<field name="name"/> <field name="name"/>
<field name="checklist_active" invisible="1"/> <field name="checklist_active"
<field name="checklist_number" invisible="1"/> invisible="1"/>
<field name="checklist_number"
invisible="1"/>
</group> </group>
</sheet> </sheet>
</form> </form>
@ -159,11 +193,13 @@
<field name="total"/> <field name="total"/>
<field name="tools_missing_cost"/> <field name="tools_missing_cost"/>
<field name="damage_cost_sub"/> <field name="damage_cost_sub"/>
<field name="total_cost" class="oe_subtotal_footer_separator"/> <field name="total_cost"
class="oe_subtotal_footer_separator"/>
</group> </group>
</div> </div>
<div style="float: right;margin-left: 78%;margin-bottom: 36px;"> <div style="float: right;margin-left: 78%;margin-bottom: 36px;">
<button name="action_verify" string="Verify" type="object" <button name="action_verify" string="Verify"
type="object"
style="width: 100px !important;height: 40px;" style="width: 100px !important;height: 40px;"
attrs="{'invisible': [('state', '!=', 'checking')]}" attrs="{'invisible': [('state', '!=', 'checking')]}"
class="oe_subtotal_footer oe_right oe_highlight"/> class="oe_subtotal_footer oe_right oe_highlight"/>
@ -207,10 +243,13 @@
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
</record> </record>
<menuitem name="Fleet Rental" id="fleet.menu_root" sequence="115" groups="fleet.fleet_group_user" <menuitem name="Fleet Rental" id="fleet.menu_root" sequence="115"
groups="fleet.fleet_group_user"
web_icon="fleet,static/description/icon.png"/> web_icon="fleet,static/description/icon.png"/>
<menuitem id="menu_car_parent" sequence="1" name="Rental Management" parent="fleet.menu_root"/> <menuitem id="menu_car_parent" sequence="1" name="Rental Management"
<menuitem id="menu_car_rental_contract" parent="menu_car_parent" name="Rental Contract" parent="fleet.menu_root"/>
<menuitem id="menu_car_rental_contract" parent="menu_car_parent"
name="Rental Contract"
action="action_car_rental_contract" sequence="1"/> action="action_car_rental_contract" sequence="1"/>
</data> </data>
</odoo> </odoo>

15
fleet_rental/views/checklist_view.xml

@ -9,8 +9,10 @@
<form string="car_checklist2" create="false"> <form string="car_checklist2" create="false">
<header> <header>
<button name="action_verify" string="Create invoice" type="object" <button name="action_verify" string="Create invoice" type="object"
attrs="{'invisible': ['|',('state','!=', 'checking'),('check_verify','=',True)]}" class="oe_highlight"/> attrs="{'invisible': ['|',('state','!=', 'checking'),('check_verify','=',True)]}"
<field name="state" widget="statusbar" statusbar_visible="checking"/> class="oe_highlight"/>
<field name="state" widget="statusbar"
statusbar_visible="checking"/>
</header> </header>
<sheet> <sheet>
<group> <group>
@ -22,13 +24,15 @@
</group> </group>
<group> <group>
<field name="damage_cost"/> <field name="damage_cost"/>
<field name="attachment_ids" widget="many2many_binary" class="oe_inline" readonly="1"/> <field name="attachment_ids" widget="many2many_binary"
class="oe_inline" readonly="1"/>
</group> </group>
</group> </group>
<notebook> <notebook>
<page string="Checklist"> <page string="Checklist">
<field name="checklist_line"> <field name="checklist_line">
<tree string="Fleet Checklist Lines" create="false" editable="bottom"> <tree string="Fleet Checklist Lines" create="false"
editable="bottom">
<field name="name"/> <field name="name"/>
<field name="checklist_active"/> <field name="checklist_active"/>
<field name="price"/> <field name="price"/>
@ -39,7 +43,8 @@
<field name="total"/> <field name="total"/>
<field name="tools_missing_cost"/> <field name="tools_missing_cost"/>
<field name="damage_cost_sub"/> <field name="damage_cost_sub"/>
<field name="total_cost" class="oe_subtotal_footer_separator"/> <field name="total_cost"
class="oe_subtotal_footer_separator"/>
</group> </group>
</div> </div>
</page> </page>

27
fleet_rental/views/res_config_settings_views.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Fleet settings form view inherited -->
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.fleet.rental</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="1"/>
<field name="inherit_id" ref="fleet.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='end_contract_setting']" position="inside">
<div class="col-12 col-lg-6 o_setting_box" id="discount_limit ">
<div class="o_setting_left_pane">
</div>
<div class="o_setting_right_pane">
<span class="o_form_label">Default Fleet Rental Service Product
</span>
<div class="row mt16">
<label for="fleet_service_product_id"
class="col-lg-4 o_light_label"/>
<field name="fleet_service_product_id" class="oe_inline"/>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save