19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:43 +01:00
parent 4607ccbd2e
commit 825ff6514e
487 changed files with 184979 additions and 195262 deletions

View file

@ -1 +1,2 @@
from . import fleet_report
from . import odometer_report

View file

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from psycopg2 import sql
from odoo import tools
from odoo import api, fields, models
from odoo import fields, models
from odoo.tools.sql import drop_view_if_exists, SQL
class FleetReport(models.Model):
_name = "fleet.vehicle.cost.report"
class FleetVehicleCostReport(models.Model):
_name = 'fleet.vehicle.cost.report'
_description = "Fleet Analysis Report"
_auto = False
_order = 'date_start desc'
@ -148,9 +147,5 @@ FROM (
contract_costs cc)
) c
"""
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(
sql.SQL("""CREATE or REPLACE VIEW {} as ({})""").format(
sql.Identifier(self._table),
sql.SQL(query)
))
drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(SQL("""CREATE or REPLACE VIEW %s as (%s)""", SQL.identifier(self._table), SQL(query)))

View file

@ -0,0 +1,233 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from psycopg2 import sql
from odoo import tools
from odoo import fields, models
class OdometerReport(models.Model):
_name = 'fleet.vehicle.odometer.report'
_description = "Fleet Odometer Analysis Report"
_auto = False
_order = 'recorded_date desc'
vehicle_id = fields.Many2one('fleet.vehicle', "Vehicle", readonly=True)
category_id = fields.Many2one(related='vehicle_id.category_id')
model_id = fields.Many2one(related='vehicle_id.model_id')
fuel_type = fields.Selection(related='vehicle_id.fuel_type')
mileage_delta = fields.Float("Mileage Delta", readonly=True)
odometer_value = fields.Float("Odometer Value", readonly=True)
recorded_date = fields.Date('Date', readonly=True)
def init(self):
query = """
-- Step 1: Get the acquisition date for each vehicle
WITH vehicle_odometer AS (
SELECT vehicle.id AS vehicle_id, odometer.value, CAST(odometer.date AS TIMESTAMP), CAST(vehicle.acquisition_date AS TIMESTAMP)
FROM fleet_vehicle_odometer odometer
LEFT JOIN fleet_vehicle vehicle ON vehicle.id=odometer.vehicle_id
),
-- Step 2: Select only one odometer record (with max value) per date per vehicle
vehicle_odometer_single_date AS (
SELECT DISTINCT ON (vehicle_id, date) t.vehicle_id, t.date, t.value, t.acquisition_date
FROM vehicle_odometer t
JOIN (
SELECT vehicle_id, date, MAX(value) AS max_value
FROM vehicle_odometer
GROUP BY vehicle_id, date
) t_max_val
ON t.vehicle_id = t_max_val.vehicle_id AND t.date = t_max_val.date AND t.value = t_max_val.max_value
),
-- Step 3: Create a fake odometer reading at 0km if the acquisition date is set and lower than every other reading
vehicle_odometer_acquisition_date AS (
SELECT vehicle_id, date, value, acquisition_date FROM vehicle_odometer_single_date
UNION ALL
(
SELECT vehicle_id, acquisition_date AS date, 0 AS value, acquisition_date
FROM vehicle_odometer_single_date
GROUP BY vehicle_id, acquisition_date
HAVING acquisition_date < MIN(date)
)
),
-- Step 4: Compute the previous and next date and value for each odometer reading
vehicle_odometer_prev_and_next AS (
SELECT vehicle_id, date, value, acquisition_date,
LAG(date) OVER (PARTITION BY vehicle_id ORDER BY date) AS prev_date,
LAG(value) OVER (PARTITION BY vehicle_id ORDER BY date) AS prev_val,
LEAD(date) OVER (PARTITION BY vehicle_id ORDER BY date) AS next_date,
LEAD(value) OVER (PARTITION BY vehicle_id ORDER BY date) AS next_val
FROM vehicle_odometer_acquisition_date
),
-- Step 5: Define the date range for each vehicle's odometer readings
vehicle_odometer_date_range AS (
SELECT
vehicle_id,
COALESCE(
CAST(DATE_TRUNC('month', MIN(acquisition_date)) AS TIMESTAMP),
CAST(DATE_TRUNC('month', MIN(date)) AS TIMESTAMP)) AS start_date,
CAST(DATE_TRUNC('month', MAX(date)) AS TIMESTAMP) AS end_date
FROM vehicle_odometer_prev_and_next
GROUP BY vehicle_id
),
-- Step 6: Generate a complete list of months for each vehicle within the date range (empty value, prev* and next*)
vehicle_odometer_date_range_min_date AS (
SELECT
date_range.vehicle_id,
CAST(COALESCE(odometer.date, generated_months.date) AS TIMESTAMP) AS date,
COALESCE(odometer.value, 0) AS value, -- Odometer value set to 0 if no reading exists
CAST(
FIRST_VALUE(COALESCE(odometer.date, generated_months.date)) OVER (
PARTITION BY date_range.vehicle_id
ORDER BY generated_months.date
) AS TIMESTAMP
) AS min_date,
odometer.prev_date,
odometer.prev_val,
odometer.next_date,
odometer.next_val
FROM vehicle_odometer_date_range date_range
CROSS JOIN LATERAL GENERATE_SERIES(
date_range.start_date,
date_range.end_date,
'1 month'::INTERVAL
) generated_months (date)
LEFT JOIN vehicle_odometer_prev_and_next odometer
ON date_range.vehicle_id = odometer.vehicle_id
AND generated_months.date = DATE_TRUNC('month', odometer.date)
),
-- Step 7: Compute the previous/next dates for every newly added readings
vehicle_odometer_prev_next_date AS (
SELECT odometer.vehicle_id, odometer.date,
MAX(COALESCE(odometer.prev_date, odo_prev_next.prev_date)) AS prev_date,
MIN(COALESCE(odometer.next_date, odo_prev_next.next_date)) AS next_date
FROM vehicle_odometer_date_range_min_date odometer
LEFT JOIN vehicle_odometer_prev_and_next odo_prev_next
ON odometer.vehicle_id = odo_prev_next.vehicle_id
AND (odo_prev_next.prev_date IS NULL OR odo_prev_next.prev_date < odometer.date)
AND (odo_prev_next.next_date IS NULL OR odo_prev_next.next_date > odometer.date)
GROUP BY odometer.vehicle_id, odometer.date
),
-- Step 8: Compute the previous/next values for every newly added readings
vehicle_odometer_prev_next_complete AS (
SELECT DISTINCT
odo_dates.vehicle_id,
odo_dates.date,
odometer.min_date,
odometer.value,
odo_dates.prev_date,
odometer_prev.value AS prev_value,
odo_dates.next_date,
odometer_next.value AS next_value
FROM vehicle_odometer_prev_next_date odo_dates
LEFT JOIN vehicle_odometer_date_range_min_date odometer ON odo_dates.vehicle_id = odometer.vehicle_id AND odo_dates.date = odometer.date
LEFT JOIN vehicle_odometer_date_range_min_date odometer_prev ON odo_dates.vehicle_id = odometer_prev.vehicle_id AND odo_dates.prev_date = odometer_prev.date
LEFT JOIN vehicle_odometer_date_range_min_date odometer_next ON odo_dates.vehicle_id = odometer_next.vehicle_id AND odo_dates.next_date = odometer_next.date
),
-- Step 9: Compute the strict previous date for each odometer reading
vehicle_odometer_strict_prev AS (
SELECT vehicle_id, date, min_date, value, prev_date, prev_value, next_date, next_value,
LAG(date) OVER (
PARTITION BY vehicle_id
ORDER BY date
) AS strict_previous_date
FROM vehicle_odometer_prev_next_complete
),
-- Step 9: Fill in gaps (interpolate) in odometer readings using previous and next known values
vehicle_odometer_filled_gaps AS (
SELECT vehicle_id, date, value,
CASE
WHEN value = 0 AND prev_value IS NOT NULL AND next_value IS NOT NULL
THEN (next_value - prev_value) * (
EXTRACT(DAY FROM (date - strict_previous_date)) /
NULLIF(EXTRACT(DAY FROM (next_date - prev_date)), 0)
)
WHEN prev_value IS NULL THEN value
ELSE (value - prev_value) * (
EXTRACT(DAY FROM (date - strict_previous_date)) /
NULLIF(EXTRACT(DAY FROM (date - prev_date)), 0)
)
END AS raw_mileage_delta
FROM vehicle_odometer_strict_prev
),
-- Step 10: Sum the interpolated mileage delta values to have an odometer per month
vehicle_odometer_interpolated AS (
SELECT
vehicle_id,
date,
CASE
WHEN value = 0 AND raw_mileage_delta IS NOT NULL
THEN SUM(raw_mileage_delta) OVER (PARTITION BY vehicle_id ORDER BY date)
ELSE value
END AS value
FROM vehicle_odometer_filled_gaps
),
-- Step 11: Calculate the days span between every odometer's reading date
vehicle_odometer_days_diff AS (
SELECT
vehicle_id,
date,
value,
value - COALESCE(LAG(value) OVER (PARTITION BY vehicle_id ORDER BY date), 0) AS raw_mileage_delta,
LAG(date) OVER (PARTITION BY vehicle_id ORDER BY date) AS prev_date,
EXTRACT(DAY FROM date::TIMESTAMP - LAG(date) OVER (PARTITION BY vehicle_id ORDER BY date)::TIMESTAMP) AS days_span
FROM vehicle_odometer_interpolated
),
-- Step 12: Compute weighted mileage for each month
vehicle_weighted_mileage AS (
SELECT
vehicle_id,
date,
value,
raw_mileage_delta,
prev_date,
days_span,
DATE_TRUNC('month', prev_date) AS prev_month,
DATE_TRUNC('month', date) AS current_month,
CASE
WHEN TO_CHAR(prev_date, 'YYYY-MM') = TO_CHAR(date, 'YYYY-MM') THEN raw_mileage_delta
ELSE raw_mileage_delta * COALESCE(
(EXTRACT(DAY FROM date::TIMESTAMP - DATE_TRUNC('month', date)::TIMESTAMP) / NULLIF(days_span, 0)), 1
)
END AS current_month_mileage,
CASE
WHEN TO_CHAR(prev_date, 'YYYY-MM') = TO_CHAR(date, 'YYYY-MM') THEN 0
ELSE raw_mileage_delta * (EXTRACT(DAY FROM DATE_TRUNC('month', date)::TIMESTAMP - prev_date::TIMESTAMP) / NULLIF(days_span, 0))
END AS prev_month_mileage
FROM vehicle_odometer_days_diff
),
-- Step 13: Aggregate final results
final_results AS (
SELECT
vehicle_id,
date AS recorded_date,
SUM(mileage_delta) OVER (PARTITION BY vehicle_id ORDER BY date) AS odometer_value,
mileage_delta
FROM (
SELECT vehicle_id, date, SUM(mileage_delta) AS mileage_delta
FROM (
SELECT vehicle_id, prev_month AS date, prev_month_mileage AS mileage_delta FROM vehicle_weighted_mileage WHERE prev_month IS NOT NULL
UNION ALL
SELECT vehicle_id, current_month AS date, current_month_mileage AS mileage_delta FROM vehicle_weighted_mileage
) t
GROUP BY vehicle_id, date
) t
),
-- Step 14: Handle the first recorded month with zero odometer
min_month AS (
SELECT vehicle_id, MIN(recorded_date) AS min_month_minus_one FROM final_results GROUP BY vehicle_id
)
-- Step 15: Generate final result set with a row number
SELECT row_number() OVER () AS id, * FROM (
SELECT vehicle_id, min_month_minus_one AS recorded_date, 0 AS odometer_value, 0 AS mileage_delta FROM min_month
UNION ALL
SELECT vehicle_id, recorded_date + INTERVAL '1 month' AS recorded_date, odometer_value, mileage_delta FROM final_results
ORDER BY vehicle_id, recorded_date
) t
"""
self.env.cr.execute(query)
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(
sql.SQL("CREATE or REPLACE VIEW {} as ({})").format(
sql.Identifier(self._table),
sql.SQL(query)))