fleetmap-reports 2.0.377 → 2.0.378

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetmap-reports",
3
- "version": "2.0.377",
3
+ "version": "2.0.378",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -187,7 +187,7 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
187
187
  }
188
188
  const dates = getDates(from, to, userData.user.attributes.timezone || getUserPartner(userData.user).timezone)
189
189
  for (const date of dates) {
190
- const { tripsByDay, routeByDay } = await getDataByDay(d, date, { trips, route: deviceRoute }, userData, traccarInstance)
190
+ const { tripsByDay, routeByDay, fromDay, toDay } = await getDataByDay(d, date, { trips, route: deviceRoute }, userData, traccarInstance)
191
191
  const distance = getKms(tripsByDay)
192
192
  if (!userData.allWeek && userData.weekDays && userData.dayHours) {
193
193
  if (weekDaySelected(date, userData.weekDays)) {
@@ -219,7 +219,10 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
219
219
  summaryCurrentDay.distance = summaryByDevice.distance
220
220
  summaryCurrentDay.averageSpeed = distance > 0 ? ((tripsByDay.reduce((a, b) => a + (b.averageSpeed * b.distance), 0)) / distance) : 0
221
221
  summaryCurrentDay.maxSpeed = tripsByDay.reduce((a, b) => { return a > b.maxSpeed ? a : b.maxSpeed }, 0)
222
- summaryCurrentDay.convertedSpentFuel = calculateConsumption(d, { route: routeByDay })
222
+ // Read the day's fuel from the CAN "fuelUsed" counter over the full route between
223
+ // fromDay/toDay (falls back to routeByDay for tank-level-only vehicles), so the
224
+ // daily figure and its total match the Manager's "Fuel Used".
225
+ summaryCurrentDay.convertedSpentFuel = calculateConsumption(d, { route: deviceRoute.length ? deviceRoute : routeByDay, from: fromDay, to: toDay })
223
226
  summaryCurrentDay.startTime = tripsByDay.length && tripsByDay[0].startTime
224
227
  summaryCurrentDay.startAddress = tripsByDay.length && tripsByDay[0].startAddress
225
228
  summaryCurrentDay.endTime = tripsByDay.length && tripsByDay[tripsByDay.length - 1].endTime
@@ -56,7 +56,7 @@ describe('activity report', function () {
56
56
  assert.equal(data.length, 1)
57
57
  const device = data[0].devices.find(d => d.device.id === 69114)
58
58
  console.log(device)
59
- assert.equal(device.summary[0].convertedSpentFuel, 168.6)
59
+ assert.ok(Math.abs(device.summary[0].convertedSpentFuel - 168.7) <= 1.5, device.summary[0].convertedSpentFuel)
60
60
  }, 800000)
61
61
  // eslint-disable-next-line no-undef
62
62
  it('Activity check fuel consumption group by day', async () => {
@@ -70,7 +70,18 @@ describe('activity report', function () {
70
70
  assert.equal(data.length, 1)
71
71
  const device = data[0].devices.find(d => d.device.id === 69114)
72
72
  console.log(device)
73
- assert.equal(device.summary.reduce((a, b) => a + b.convertedSpentFuel, 0), 168.79999999999995)
73
+ // group-by-day must add up to the same "Fuel Used" total as the single-period value
74
+ const groupedTotal = device.summary.reduce((a, b) => a + b.convertedSpentFuel, 0)
75
+
76
+ const ungrouped = await getReports().then(async (r) => {
77
+ const ud = await r.getUserData()
78
+ ud.devices = ud.devices.filter(d => d.id === 69114)
79
+ const dd = await r.activityReport(new Date(2023, 5, 11, 0, 0, 0, 0), new Date(2023, 6, 11, 23, 59, 59, 0), ud)
80
+ return dd[0].devices.find(d => d.device.id === 69114).summary[0].convertedSpentFuel
81
+ })
82
+
83
+ assert.ok(Math.abs(groupedTotal - ungrouped) <= 1, `grouped ${groupedTotal} vs ungrouped ${ungrouped}`)
84
+ assert.ok(Math.abs(groupedTotal - 168.7) <= 1.5, groupedTotal)
74
85
  }, 800000)
75
86
  // eslint-disable-next-line no-undef
76
87
  it('works with timetable', async () => {
@@ -0,0 +1,61 @@
1
+ /* eslint-disable no-undef */
2
+ const assert = require('assert')
3
+ const { getFuelUsedBetween, calculateConsumption } = require('../util/fuel')
4
+
5
+ function pos (iso, fuelUsed, extra = {}) {
6
+ return { fixTime: new Date(iso).toISOString(), attributes: { fuelUsed, ...extra } }
7
+ }
8
+
9
+ // Reproduces ticket "fuel consumption of a full day not taken into account":
10
+ // a CAN vehicle that refuels mid-period. Traccar's daily summary spentFuel goes
11
+ // negative on the refuel day, but the "fuelUsed" cumulative counter (shown as
12
+ // "Fuel Used" in the Manager) keeps rising and is the source of truth.
13
+ const route = [
14
+ pos('2026-06-01T06:00:00Z', 1000),
15
+ pos('2026-06-01T20:00:00Z', 1116), // +116 day 1
16
+ pos('2026-06-02T07:00:00Z', 1130, { fuel: 40 }),
17
+ pos('2026-06-02T10:00:00Z', 1150, { fuel: 250 }), // refuelled
18
+ pos('2026-06-02T19:00:00Z', 1227, { fuel: 210 }), // +111 day 2
19
+ { fixTime: new Date('2026-06-03T05:30:00Z').toISOString(), attributes: {} }, // no counter on first fix
20
+ pos('2026-06-03T18:00:00Z', 1269) // +42 day 3
21
+ ]
22
+ const device = { attributes: {} }
23
+ const day = (from, to) => [new Date(from).toISOString(), new Date(to).toISOString()]
24
+
25
+ describe('fuelUsed counter based consumption', function () {
26
+ it('uses the fuelUsed counter for each day and ignores a negative summary', () => {
27
+ const [f1, t1] = day('2026-06-01T00:00:00Z', '2026-06-01T23:59:59Z')
28
+ const [f2, t2] = day('2026-06-02T00:00:00Z', '2026-06-02T23:59:59Z')
29
+ const [f3, t3] = day('2026-06-03T00:00:00Z', '2026-06-03T23:59:59Z')
30
+
31
+ const d1 = calculateConsumption(device, { route, summary: { spentFuel: -170 }, from: f1, to: t1 })
32
+ const d2 = calculateConsumption(device, { route, summary: { spentFuel: -170 }, from: f2, to: t2 })
33
+ const d3 = calculateConsumption(device, { route, summary: { spentFuel: 0 }, from: f3, to: t3 })
34
+
35
+ assert.strictEqual(d1, 116)
36
+ assert.strictEqual(d2, 111) // refuel day is no longer dropped
37
+ assert.strictEqual(d3, 42) // missing attribute on first fix is no longer dropped
38
+ })
39
+
40
+ it('daily values sum to the period total shown as "Fuel Used"', () => {
41
+ const [f1, t1] = day('2026-06-01T00:00:00Z', '2026-06-01T23:59:59Z')
42
+ const [f2, t2] = day('2026-06-02T00:00:00Z', '2026-06-02T23:59:59Z')
43
+ const [f3, t3] = day('2026-06-03T00:00:00Z', '2026-06-03T23:59:59Z')
44
+ const sum = calculateConsumption(device, { route, from: f1, to: t1 }) +
45
+ calculateConsumption(device, { route, from: f2, to: t2 }) +
46
+ calculateConsumption(device, { route, from: f3, to: t3 })
47
+ const period = calculateConsumption(device, { route, summary: { spentFuel: -170 } })
48
+
49
+ assert.strictEqual(period, 269) // 1269 - 1000
50
+ assert.strictEqual(Number(sum.toFixed(3)), period)
51
+ })
52
+
53
+ it('clamps a counter reset to 0', () => {
54
+ assert.strictEqual(getFuelUsedBetween([pos('2026-06-01T06:00:00Z', 5000), pos('2026-06-01T12:00:00Z', 30)]), 0)
55
+ })
56
+
57
+ it('falls back to summary spentFuel when the route has no fuelUsed', () => {
58
+ const noCan = [{ fixTime: new Date('2026-06-01T06:00:00Z').toISOString(), attributes: {} }]
59
+ assert.strictEqual(calculateConsumption(device, { route: noCan, summary: { spentFuel: 12 } }), 12)
60
+ })
61
+ })
@@ -216,7 +216,7 @@ async function processDevices (from, to, devices, data, userData, traccar) {
216
216
 
217
217
  if (deviceRoute.some(r => r.attributes.fuel) ||
218
218
  automaticReports.deviceWithFuelInfo(d)) {
219
- trip.fuelConsumption = calculateConsumption(d, { trips: [trip], stops: (stop ? [stop] : []), route: deviceRoute })
219
+ trip.fuelConsumption = calculateConsumption(d, { trips: [trip], stops: (stop ? [stop] : []), route: deviceRoute, from: trip.startTime, to: trip.endTime })
220
220
  trip.avgFuelConsumption = trip.totalKms > 0 ? trip.fuelConsumption * 100 / trip.totalKms : 0
221
221
  }
222
222
 
package/src/util/fuel.js CHANGED
@@ -57,15 +57,76 @@ function calculateTotalFuelFromSensor(trips, stops, positions, device, firstStop
57
57
  }
58
58
 
59
59
 
60
+ function getPositionFuelUsed (position) {
61
+ const raw = position && position.attributes ? position.attributes.fuelUsed : undefined
62
+ const value = typeof raw === 'string' ? Number.parseFloat(raw) : raw
63
+ return typeof value === 'number' && !Number.isNaN(value) ? value : null
64
+ }
65
+
66
+ /**
67
+ * Fuel consumed over a time window, read straight from the CAN "fuelUsed" cumulative
68
+ * counter - the exact value the Manager shows as "Fuel Used".
69
+ *
70
+ * This is the single source of truth for every report whenever the route carries
71
+ * "fuelUsed". It must NOT be derived from the daily summary "spentFuel" (which Traccar
72
+ * computes from the tank fuel-level and returns as <= 0 on any day the vehicle was
73
+ * refuelled, making a whole day silently drop out of the report).
74
+ *
75
+ * Robust against:
76
+ * - positions without the fuelUsed attribute (e.g. the first/last fix of a day)
77
+ * - refuelling (a cumulative counter never goes down, unlike a tank-level reading)
78
+ * - the window opening between two fixes (uses the last known counter before it, so
79
+ * consecutive days partition the period with no gap and sum to the period total)
80
+ * - counter resets (negative overall difference is clamped to 0, as before)
81
+ *
82
+ * @param {Array} route positions for a single device (any order)
83
+ * @param {Date|string|number} [from] window start; omit for "from the first fix"
84
+ * @param {Date|string|number} [to] window end; omit for "until the last fix"
85
+ */
86
+ function getFuelUsedBetween (route, from, to) {
87
+ if (!Array.isArray(route)) { return 0 }
88
+
89
+ const fromTime = from != null ? new Date(from).getTime() : null
90
+ const toTime = to != null ? new Date(to).getTime() : null
91
+
92
+ const samples = []
93
+ for (const p of route) {
94
+ const value = getPositionFuelUsed(p)
95
+ if (value == null) { continue }
96
+ samples.push({ t: new Date(p.fixTime).getTime(), value })
97
+ }
98
+ if (samples.length < 2) { return 0 }
99
+ samples.sort((a, b) => a.t - b.t)
100
+
101
+ // end = last counter reading at or before the end of the window
102
+ let end = null
103
+ for (const s of samples) {
104
+ if (toTime == null || s.t <= toTime) { end = s.value } else { break }
105
+ }
106
+
107
+ // start = last counter reading at or before the start of the window,
108
+ // or the first reading inside it when the window opens before any fix
109
+ let start = null
110
+ for (const s of samples) {
111
+ if (fromTime == null) { start = s.value; break }
112
+ if (s.t <= fromTime) { start = s.value } else { if (start == null) { start = s.value } break }
113
+ }
114
+
115
+ if (start == null || end == null) { return 0 }
116
+ const diff = end - start
117
+ return diff > 0 ? diff : 0
118
+ }
119
+
60
120
  function calculateConsumption (d, data) {
61
121
  let fuelConsumption = 0
62
122
  const withFuelUsed = data.route && data.route.find(p => p.attributes.fuelUsed)
63
- if (data.summary && withFuelUsed) {
123
+ if (withFuelUsed && data.route.length > 1) {
124
+ // CAN "fuelUsed" counter - keep every report aligned with the Manager's "Fuel Used".
125
+ fuelConsumption = getFuelUsedBetween(data.route, data.from, data.to)
126
+ } else if (data.summary && withFuelUsed) {
64
127
  fuelConsumption = calculateSpentFuel(data.summary.spentFuel, d, withFuelUsed)
65
128
  } else if (data.trips && data.stops) {
66
129
  fuelConsumption = data.trips.reduce((a, b) => a + calculateSpentFuel(b.spentFuel, d, withFuelUsed), 0) + data.stops.reduce((a, b) => a + calculateSpentFuel(b.spentFuel, d, withFuelUsed), 0)
67
- } else if (data.route && data.route.length > 1 && data.route[0].attributes.fuelUsed) {
68
- fuelConsumption = data.route[data.route.length - 1].attributes.fuelUsed - data.route[0].attributes.fuelUsed
69
130
  } else if (data.summary) {
70
131
  fuelConsumption = data.summary.spentFuel
71
132
  }
@@ -98,5 +159,6 @@ function useOdooFuelData (d, route) {
98
159
  exports.calculateFuelDiff = calculateFuelDiff
99
160
  exports.getRefuelingLiters = getRefuelingLiters
100
161
  exports.calculateConsumption = calculateConsumption
162
+ exports.getFuelUsedBetween = getFuelUsedBetween
101
163
  exports.useOdooFuelData = useOdooFuelData
102
- exports.calculateTotalFuelFromSensor = calculateTotalFuelFromSensor
164
+ exports.calculateTotalFuelFromSensor = calculateTotalFuelFromSensor
package/src/util/trips.js CHANGED
@@ -112,7 +112,7 @@ async function getDataByDay (device, date, data, userData, traccarInstance) {
112
112
  (!z.outTime || (new Date(z.outTime.fixTime) > startDate)))
113
113
  : []
114
114
 
115
- return { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay, zoneInOutDayData }
115
+ return { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay, zoneInOutDayData, fromDay: startDate, toDay: endDate }
116
116
  }
117
117
 
118
118
  function isValidAvgSpeed (kms, startTime, endTime) {