oca-ocb-vertical-industry/odoo-bringout-oca-ocb-fleet/fleet/report/odometer_report.py
Ernad Husremovic 825ff6514e 19.0 vanilla
2026-03-09 09:32:43 +01:00

233 lines
12 KiB
Python

# 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)))