Skip to content

Commit

Permalink
Added hr_expense_fleet module
Browse files Browse the repository at this point in the history
  • Loading branch information
ByteMeAsap committed Feb 16, 2024
1 parent 666d5ee commit bc8394c
Show file tree
Hide file tree
Showing 70 changed files with 3,679 additions and 0 deletions.
1 change: 1 addition & 0 deletions hr_expense_fleet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
21 changes: 21 additions & 0 deletions hr_expense_fleet/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2024 Onestein (<http://www.onestein.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Hr Expense Fleet",
"version": "16.0.1.0.0",
"category": "Human Resources/Expenses",
"license": "LGPL-3",
"summary": "Allows to create expenses for fleet",
"depends": [
"hr_expense",
"hr_fleet",
"product_analytic"
],
"data": [
"data/hr_expense_data.xml",
"views/fleet_vehicle_odometer_view.xml",
"views/fleet_vehicle_view.xml",
"views/hr_expense_view.xml",
"views/product_view.xml",
],
}
9 changes: 9 additions & 0 deletions hr_expense_fleet/data/hr_expense_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="hr_expense.expense_product_mileage" model="product.product">
<field name="can_be_used_for_fleet" eval="True"/>
<field name="uom_id" ref="uom.product_uom_km"/>
</record>
</data>
</odoo>
4 changes: 4 additions & 0 deletions hr_expense_fleet/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import fleet_vehicle
from . import fleet_vehicle_odometer
from . import hr_expense
from . import product_template
9 changes: 9 additions & 0 deletions hr_expense_fleet/models/fleet_vehicle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from odoo import api, fields, models


class FleetVehicle(models.Model):
_inherit = "fleet.vehicle"

product_id = fields.Many2one("product.product", string="Expense Category",
domain="[('can_be_expensed', '=', True),('can_be_used_for_fleet', '=', True)]",
help="Defines default expense category for this vehicle's trips")
74 changes: 74 additions & 0 deletions hr_expense_fleet/models/fleet_vehicle_odometer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from odoo import api, fields, models


class FleetVehicleOdometer(models.Model):
_inherit = "fleet.vehicle.odometer"

partner_id = fields.Many2one("res.partner", "Contact Visited",
help="Defines the contact visited for the trip if any")
from_address = fields.Text("From", help="Defines the from address for the trip")
to_address = fields.Text("To", help="Defines the to address for the trip")
distance = fields.Float("Distance For Single Way Trip", help="Defines the distance covered for the trip")
is_roundtrip = fields.Boolean("Is Roundtrip", help="Defines whether it is a round trip or not")
total_distance = fields.Float("Total Distance", compute="_compute_total_distance",
help="Defines the total distance covered including round trip")
is_private_trip = fields.Boolean("Is Private Trip", help="Defines whether it is a private trip or not")
value = fields.Float("Odometer End Value", group_operator="max", copy=False, )
start_value = fields.Float("Odometer Start Value", group_operator="max", copy=False, )
expense_id = fields.Many2one("hr.expense", "Expense", help="Defines expense record for this trip", copy=False)
product_id = fields.Many2one("product.product", string="Expense Category",
domain="[('can_be_expensed', '=', True),('can_be_used_for_fleet', '=', True)]",
help="Defines expense category for this trip")
status = fields.Selection(
[("not_to_expense", "Not To Expense"), ("to_expense", "To Expense"), ("expense_created", "Expense Created")],
"Status", help="Defines expense status for this trip", default="to_expense", copy=False)

@api.depends("distance", "is_roundtrip")
def _compute_total_distance(self):
for rec in self:
rec.total_distance = rec.distance * 2 if rec.is_roundtrip else rec.distance

@api.onchange("is_private_trip")
def _onchange_is_private_trip(self):
if self.is_private_trip:
self.status = "not_to_expense"
else:
self.status = "to_expense"

@api.onchange("vehicle_id")
def _onchange_vehicle(self):
super()._onchange_vehicle()
if self.vehicle_id:
self.product_id = self.vehicle_id.product_id

def action_create_expense(self):
hr_expense_obj = self.env["hr.expense"]
product_uom_km = self.env.ref("uom.product_uom_km")
product_uom_mi = self.env.ref("uom.product_uom_mile")
to_expense_odometers = self.filtered(lambda o: o.status == "to_expense" and (
o.driver_employee_id == self.env.user.employee_id or not o.driver_employee_id))
products = to_expense_odometers.mapped("product_id")
for product in products:
odometers = to_expense_odometers.filtered(lambda o: o.product_id == product)
product_uom = product.uom_id
if product_uom == product_uom_km:
product_unit = "kilometers"
odometer_uom_to_convert = product_uom_mi
else:
product_unit = "miles"
odometer_uom_to_convert = product_uom_km
total_distance = sum(
line.total_distance for line in odometers.filtered(lambda ol: ol.unit == product_unit))
for odometer in odometers.filtered(lambda ol: ol.unit != product_unit):
total_distance += odometer_uom_to_convert._compute_quantity(odometer.total_distance, product_uom)
ana_accounts = product.product_tmpl_id._get_product_analytic_accounts()
ana_account = ana_accounts["expense"]
hr_expense_rec = hr_expense_obj.create({
"product_id": product.id,
"quantity": total_distance,
"analytic_distribution": (
{ana_account.id: 100} if ana_account else False
)
})
odometers.write({"expense_id": hr_expense_rec.id, "status": "expense_created"})
return True
24 changes: 24 additions & 0 deletions hr_expense_fleet/models/hr_expense.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from odoo import api, fields, models


class Expense(models.Model):
_inherit = "hr.expense"

fleet_vehicle_odometer_ids = fields.One2many("fleet.vehicle.odometer", "expense_id", "OdoMeters", copy=False)
odometer_count = fields.Integer(compute="_compute_odometer_count", string="Odometer")

@api.depends("fleet_vehicle_odometer_ids")
def _compute_odometer_count(self):
for rec in self:
rec.odometer_count = len(rec.fleet_vehicle_odometer_ids)

def open_odometer(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": "Odometers",
"res_model": "fleet.vehicle.odometer",
"view_mode":"tree,kanban,form,graph",
"domain": [("id", "in", self.fleet_vehicle_odometer_ids.ids)],
"context": {"create": False, "edit": False},
}
23 changes: 23 additions & 0 deletions hr_expense_fleet/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from odoo import api, fields, models


class ProductProduct(models.Model):
_inherit = "product.template"

can_be_used_for_fleet = fields.Boolean("Can Be Used For Fleet",
help="Helps to define whether this product is to be used for fleet or not")
uom_id_domain = fields.Binary(string="UOM Domain",
help="Dynamic domain used for the uom that can be set on product to be used for fleet",
compute="_compute_uom_id_domain")

@api.depends("can_be_used_for_fleet")
def _compute_uom_id_domain(self):
ids = [self.env.ref("uom.product_uom_km").id, self.env.ref("uom.product_uom_mile").id]
for rec in self:
rec.uom_id_domain = [("id", "in", ids)] if rec.can_be_used_for_fleet else []

@api.onchange("can_be_used_for_fleet")
def _onchange_can_be_used_for_fleet(self):
if self.can_be_used_for_fleet:
if self.uom_id not in [self.env.ref("uom.product_uom_km"), self.env.ref("uom.product_uom_mile")]:
self.uom_id = self.env.ref("uom.product_uom_km").id
1 change: 1 addition & 0 deletions hr_expense_fleet/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_hr_expense_fleet
77 changes: 77 additions & 0 deletions hr_expense_fleet/tests/test_hr_expense_fleet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
from odoo.tests import common


class TestHrExpenseFleet(common.SingleTransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product_mileage = cls.env.ref("hr_expense.expense_product_mileage")
cls.product_expense = cls.env["product.product"].create({
"name": "Travel",
"default_code": "EXP_TRA",
"standard_price": 0.93,
"can_be_expensed": True,
})
cls.env.user.groups_id += cls.env.ref("uom.group_uom")
cls.env.user.action_create_employee()
brand = cls.env["fleet.vehicle.model.brand"].create({
"name": "Audi",
})
model = cls.env["fleet.vehicle.model"].create({
"brand_id": brand.id,
"name": "A3",
})
cls.vehicle_with_km_odometer = cls.env["fleet.vehicle"].create({
"model_id": model.id,
"driver_id": cls.env.user.partner_id.id,
"product_id": cls.product_mileage.id
})
model = cls.env["fleet.vehicle.model"].create({
"brand_id": brand.id,
"name": "A8",
})
cls.vehicle_with_mi_odometer = cls.env["fleet.vehicle"].create({
"model_id": model.id,
"driver_id": cls.env.user.partner_id.id,
})

def test_01_onchange_product_can_be_used_for_fleet(self):
self.assertEqual(self.product_expense.uom_id, self.env.ref("uom.product_uom_unit"))
self.assertEqual(self.product_expense.uom_id_domain, [])
self.product_expense.can_be_used_for_fleet = True
self.product_expense.product_tmpl_id._onchange_can_be_used_for_fleet()
uom_km = self.env.ref("uom.product_uom_km")
uom_mile = self.env.ref("uom.product_uom_mile")
self.assertEqual(self.product_expense.uom_id, uom_km)
self.assertEqual(self.product_expense.uom_id_domain, [("id", "in", [uom_km.id, uom_mile.id])])
self.product_expense.uom_id = self.env.ref("uom.product_uom_mile").id

def test_01_onchange_odometer(self):
self.vehicle_with_mi_odometer.product_id = self.product_expense.id
self.odometer_in_mi = self.env["fleet.vehicle.odometer"].create(
{"vehicle_id": self.vehicle_with_mi_odometer.id,
"from_address": "Breda", "to_address": "Tilburg", "distance": 10.0, "is_roundtrip": True})
self.assertEqual(self.odometer_in_mi.total_distance, 20.0)
self.odometer_in_mi._onchange_vehicle()
self.assertEqual(self.odometer_in_mi.product_id, self.vehicle_with_mi_odometer.product_id)
self.odometer_in_mi.is_private_trip = True
self.odometer_in_mi._onchange_is_private_trip()
self.assertEqual(self.odometer_in_mi.status, "not_to_expense")
self.odometer_in_mi.is_private_trip = False
self.odometer_in_mi._onchange_is_private_trip()
self.assertEqual(self.odometer_in_mi.status, "to_expense")

def test_action_create_expense(self):
self.odometer_in_km = self.env["fleet.vehicle.odometer"].create(
{"vehicle_id": self.vehicle_with_km_odometer.id,
"from_address": "Breda", "to_address": "Tilburg", "distance": 10.0})
self.odometer_in_km._onchange_vehicle()
self.odometer_in_mi = self.env["fleet.vehicle.odometer"].create(
{"vehicle_id": self.vehicle_with_mi_odometer.id,
"from_address": "Breda", "to_address": "Tilburg", "distance": 10.0, "is_roundtrip": True})
self.odometer_in_mi._onchange_vehicle()
(self.odometer_in_km + self.odometer_in_mi).action_create_expense()
self.assertEqual(self.odometer_in_mi.status, "expense_created")
self.assertEqual(self.odometer_in_mi.expense_id.quantity, 12.43)
self.assertEqual(self.odometer_in_km.expense_id.quantity, 10.0)
108 changes: 108 additions & 0 deletions hr_expense_fleet/views/fleet_vehicle_odometer_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2024 Onestein (<https://www.onestein.eu>)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="fleet_vehicle_odometer_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.odometer.form</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="inherit_id" ref="fleet.fleet_vehicle_odometer_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form/sheet" position="before">
<header>
<field name="status" widget="statusbar"/>
</header>
</xpath>
<xpath expr="//label[@for='value']" position="replace"/>
<xpath expr="//div[hasclass('o_row')]" position="replace"/>
<xpath expr="//sheet/group" position="inside">
<group>
<field name="is_roundtrip" attrs="{'readonly':[('status','=','expense_created')]}"/>
<field name="total_distance"/>
<field name="is_private_trip" attrs="{'readonly':[('status','=','expense_created')]}"/>
<field name="product_id"
attrs="{'required':[('is_private_trip','=',False)],'readonly':[('status','=','expense_created')]}"
options="{'no_create': True, 'no_open': True}"/>
<xpath expr="//label[@for='value']" position="move"/>
<xpath expr="//div[hasclass('o_row')]" position="move"/>
<label for="start_value"/>
<div class="o_row">
<field name="start_value" class="oe_inline"
attrs="{'readonly':[('status','=','expense_created')]}"/>
<field name="unit" class="ms-2"/>
</div>
<label for="value"/>
<div class="o_row">
<field name="value" class="oe_inline"/>
<field name="unit" class="ms-2"/>
</div>
<field name="expense_id" readonly="True"/>
</group>
</xpath>
<field name="vehicle_id" position="after">
<xpath expr="//field[@name='date']" position="move"/>
<field name="partner_id"/>
<field name="from_address"
attrs="{'required':[('is_private_trip','=',False)],'readonly':[('status','=','expense_created')]}"/>
<field name="to_address"
attrs="{'required':[('is_private_trip','=',False)],'readonly':[('status','=','expense_created')]}"/>
<field name="distance"
attrs="{'required':[('is_private_trip','=',False)],'readonly':[('status','=','expense_created')]}"/>
</field>
<field name="date" position="attributes">
<attribute name="attrs">{'readonly':[('status','=','expense_created')]}</attribute>
</field>
<field name="value" position="attributes">
<attribute name="attrs">{'readonly':[('status','=','expense_created')]}</attribute>
</field>
<field name="vehicle_id" position="attributes">
<attribute name="attrs">{'readonly':[('status','=','expense_created')]}</attribute>
</field>
</field>
</record>

<record id="fleet_vehicle_odometer_view_tree" model="ir.ui.view">
<field name="name">fleet.vehicle.odometer.tree</field>
<field name="model">fleet.vehicle.odometer</field>
<field name="inherit_id" ref="fleet.fleet_vehicle_odometer_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="editable"/>
</xpath>
<field name="value" position="attributes">
<attribute name="invisible">True</attribute>
</field>
<field name="unit" position="attributes">
<attribute name="invisible">True</attribute>
</field>
<field name="date" position="after">
<field name="partner_id"/>
<field name="total_distance"/>
<field name="product_id" attrs="{'required':[('is_private_trip','=',False)],'readonly':[('status','=','expense_created')]}" options="{'no_create': True, 'no_open': True}"/>
<field name="status" readonly="True"/>
<xpath expr="//field[@name='vehicle_id']" position="move"/>
<xpath expr="//field[@name='driver_id']" position="move"/>
<field name="is_private_trip" optional="hide"/>
<field name="expense_id" optional="hide"/>
</field>
<field name="vehicle_id" position="attributes">
<attribute name="optional">hide</attribute>
</field>
<field name="driver_id" position="attributes">
<attribute name="optional">hide</attribute>
</field>
</field>
</record>

<record id="action_create_expense" model="ir.actions.server">
<field name="name">Create Expense From Odometer</field>
<field name="model_id" ref="fleet.model_fleet_vehicle_odometer"/>
<field name="binding_model_id" ref="fleet.model_fleet_vehicle_odometer"/>
<field name="binding_view_types">list,form</field>
<field name="state">code</field>
<field name="code">
records.action_create_expense()
</field>
</record>
</odoo>
17 changes: 17 additions & 0 deletions hr_expense_fleet/views/fleet_vehicle_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2024 Onestein (<https://www.onestein.eu>)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="fleet_vehicle_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.form</field>
<field name="model">fleet.vehicle</field>
<field name="inherit_id" ref="fleet.fleet_vehicle_view_form"/>
<field name="arch" type="xml">
<field name="category_id" position="after">
<field name="product_id" options="{'no_create': True, 'no_open': True}"/>
</field>
</field>
</record>
</odoo>
Loading

0 comments on commit bc8394c

Please sign in to comment.