File size: 30,085 Bytes
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
8d155aa
 
 
edaf00b
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
 
edaf00b
 
8d155aa
 
 
 
 
 
 
edaf00b
 
 
 
 
8d155aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf00b
8d155aa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
{
  "generated_utc": "2026-09-16T07:59:24.115329+00:00",
  "stack": "odoo",
  "subject": "the 8 python.* rules composed into the Odoo stack",
  "checkouts": {
    "OpenSPP2-19.0": "cfa0dc8e",
    "server-tools": "60cd54e5e",
    "account-financial-tools": "e4c1b86aa"
  },
  "lines": {
    "OpenSPP2-19.0": 226937,
    "OCA": 33960
  },
  "totals": {
    "true": 9,
    "false": 42
  },
  "by_rule": {
    "python.assert-is-not-validation": {
      "true": 6,
      "false": 4
    },
    "python.decimal-for-money-not-float": {
      "true": 0,
      "false": 3
    },
    "python.eval-and-pickle-execute-their-input": {
      "true": 0,
      "false": 1
    },
    "python.naive-and-aware-datetimes-do-not-compare": {
      "true": 3,
      "false": 30
    },
    "python.path-join-does-not-contain-a-path": {
      "true": 0,
      "false": 4
    }
  },
  "classes": {
    "validates-caller-data": 6,
    "internal-invariant": 3,
    "odoo-monetary-is-float": 3,
    "matched-inside-a-docstring": 2,
    "display-only": 8,
    "naive-utc-across-an-api-boundary": 2,
    "utc-by-odoo": 15,
    "default-never-reached": 2,
    "self-consistent-utc": 2,
    "no-user-supplied-component": 4,
    "re-enforced-on-the-next-line": 1,
    "created-a-day-ahead": 1,
    "server-date-as-a-user-date": 1,
    "self-consistent-local": 1
  },
  "hits": [
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OpenSPP2-19.0",
      "path": "endpoint_route_handler/registry.py",
      "line": 314,
      "verdict": "true",
      "class": "validates-caller-data",
      "why": "options come back off a jsonb row in from_row(), so this asserts about stored data rather than a program invariant; under -O a malformed handler dict is accepted silently",
      "evidence": "assert \"klass_dotted_path\" in value[\"handler\"]"
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OpenSPP2-19.0",
      "path": "endpoint_route_handler/registry.py",
      "line": 315,
      "verdict": "true",
      "class": "validates-caller-data",
      "why": "same setter, same stored data, second key",
      "evidence": "assert \"method_name\" in value[\"handler\"]"
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OpenSPP2-19.0",
      "path": "spp_registry/models/reg_relationship.py",
      "line": 165,
      "verdict": "false",
      "class": "internal-invariant",
      "why": "_check_partner is called from two @api.constrains with the literal 'source' and 'destination'; the assert is impossible unless the module itself is broken, which is what assert is for",
      "evidence": "assert side in [\"source\", \"destination\"]"
    },
    {
      "rule": "python.decimal-for-money-not-float",
      "tree": "OpenSPP2-19.0",
      "path": "spp_programs/models/cel/entitlement_amount_cel.py",
      "line": 300,
      "verdict": "false",
      "class": "odoo-monetary-is-float",
      "why": "self.amount is a Monetary field, which the ORM defines AS a float; the rule's remedy (Decimal) is not the Odoo idiom and float_round/float_compare are",
      "evidence": "\"base_amount\": float(self.amount or 0.0),"
    },
    {
      "rule": "python.decimal-for-money-not-float",
      "tree": "OpenSPP2-19.0",
      "path": "spp_programs/models/cel/entitlement_amount_cel.py",
      "line": 316,
      "verdict": "false",
      "class": "odoo-monetary-is-float",
      "why": "the CEL result is coerced for a Monetary field; same reason",
      "evidence": "amount = float(result)"
    },
    {
      "rule": "python.eval-and-pickle-execute-their-input",
      "tree": "OpenSPP2-19.0",
      "path": "spp_cel_domain/models/cel_service.py",
      "line": 417,
      "verdict": "false",
      "class": "matched-inside-a-docstring",
      "why": "the text is prose in a method docstring -- 'more robust and secure than regex-based conversion to Python eval()' -- and there is no eval call here at all",
      "evidence": "and secure than regex-based conversion to Python eval()."
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "scripts/compliance/checker.py",
      "line": 745,
      "verdict": "false",
      "class": "display-only",
      "why": "a Generated: line in a markdown report; formatted once, never compared",
      "evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\","
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "scripts/compliance/checker.py",
      "line": 813,
      "verdict": "false",
      "class": "display-only",
      "why": "the same timestamp in the JSON version of that report",
      "evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat(),"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_analytics/models/service_aggregation.py",
      "line": 97,
      "verdict": "false",
      "class": "display-only",
      "why": "computed_at in a result dict that is returned, not stored or compared",
      "evidence": "\"computed_at\": datetime.now().isoformat(),"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_api_v2/routers/consent.py",
      "line": 178,
      "verdict": "true",
      "class": "naive-utc-across-an-api-boundary",
      "why": "datetime.utcnow() is naive, is returned in a ConsentRevokeResponse, and serialises with no offset, so the consumer cannot tell the zone; deprecated in 3.12 for this reason",
      "evidence": "revoked_at=datetime.utcnow(),"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_api_v2/routers/consent.py",
      "line": 270,
      "verdict": "true",
      "class": "naive-utc-across-an-api-boundary",
      "why": "the consent receipt timestamp, same construct. SUPPRESSED, and wrongly: the marker inside that return statement reads 'nosemgrep: odoo-sudo-without-context' and is about a different rule",
      "evidence": "timestamp=datetime.utcnow(),",
      "suppressed": true
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_api_v2/routers/oauth.py",
      "line": 217,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "utcnow() and now() are the same value inside Odoo, and PyJWT reads a naive exp/iat as UTC, so the token lifetime is right. inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "now = datetime.utcnow()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_api_v2/services/group_service.py",
      "line": 142,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "now = datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_api_v2/services/group_service.py",
      "line": 748,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "ended_datetime = datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_api_v2/services/group_service.py",
      "line": 886,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "now = datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_api_v2/services/group_service.py",
      "line": 1100,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "now = datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_attendance/controllers/controllers.py",
      "line": 198,
      "verdict": "false",
      "class": "default-never-reached",
      "why": "the server-date default for submitted_datetime sits on the line after check_required_fields rejects a request without it, so it is never used. Re-read 2026-09-26; was classified true.",
      "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_attendance/controllers/controllers.py",
      "line": 351,
      "verdict": "false",
      "class": "default-never-reached",
      "why": "the same unreachable default on the second endpoint. Re-read 2026-09-26; was classified true.",
      "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_audit/models/spp_audit_backend.py",
      "line": 198,
      "verdict": "false",
      "class": "self-consistent-utc",
      "why": "utcnow() names an audit file and the rotation check compares it against a value from the same call; nothing aware is ever involved",
      "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_audit/models/spp_audit_backend.py",
      "line": 216,
      "verdict": "false",
      "class": "self-consistent-utc",
      "why": "the same value, in the rotation check itself",
      "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_cel_domain/services/cel_functions.py",
      "line": 92,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "return datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_cel_domain/services/cel_parser.py",
      "line": 757,
      "verdict": "false",
      "class": "matched-inside-a-docstring",
      "why": "a doctest example inside the docstring of hours_since, not code",
      "evidence": ">>> two_hours_ago = datetime.now() - timedelta(hours=2)"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_cel_domain/services/cel_parser.py",
      "line": 766,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "delta = datetime.now() - datetime_value",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_dci_server/middleware/rate_limit.py",
      "line": 52,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "now = datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_encryption/models/encryption_provider.py",
      "line": 110,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "curr_datetime = f\"{datetime.now().isoformat(timespec='milliseconds')}Z\"",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_farmer_registry_demo/models/seeded_farm_generator.py",
      "line": 1127,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "enrollment_dt = datetime.datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_grm_demo/models/generate_tickets.py",
      "line": 922,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "now = datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_mis_demo_v2/models/mis_demo_generator.py",
      "line": 3388,
      "verdict": "false",
      "class": "display-only",
      "why": "strftime('%Y-%m') builds a period label",
      "evidence": "current_period = datetime.datetime.now().strftime(\"%Y-%m\")"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_mis_demo_v2/models/mis_demo_generator.py",
      "line": 3390,
      "verdict": "false",
      "class": "display-only",
      "why": "the same label for the previous three months",
      "evidence": "(datetime.datetime.now() - datetime.timedelta(days=30 * i)).strftime(\"%Y-%m\")"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_mis_demo_v2/models/mis_demo_generator.py",
      "line": 3740,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "now = datetime.datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_mis_demo_v2/models/seeded_volume_generator.py",
      "line": 406,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "enrollment_dt = datetime.datetime.now()",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OpenSPP2-19.0",
      "path": "spp_programs/models/managers/program_manager.py",
      "line": 141,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "new_cycle = cm.new_cycle(\"Cycle 1\", datetime.now(), 1)",
      "was": "true"
    },
    {
      "rule": "python.path-join-does-not-contain-a-path",
      "tree": "OpenSPP2-19.0",
      "path": "spp_drims_sl_demo/wizard/drims_demo_generator.py",
      "line": 486,
      "verdict": "false",
      "class": "no-user-supplied-component",
      "why": "file_config['filename'] comes from a list of literals declared in the same function; there is nothing here a caller can point out of the directory",
      "evidence": "file_path = os.path.join(data_path, file_config[\"filename\"])"
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OCA",
      "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py",
      "line": 265,
      "verdict": "true",
      "class": "validates-caller-data",
      "why": "confirm_order() is a public method reachable over XML-RPC; under -O the operation-type guard is gone and the wrong record is confirmed",
      "evidence": "assert self.operation_type == \"order\", \"Wrong operation type\""
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OCA",
      "path": "account-financial-tools/account_cash_deposit/wizards/account_cash_order_reception.py",
      "line": 27,
      "verdict": "true",
      "class": "validates-caller-data",
      "why": "the assert is about self._context, which the client supplies; under -O default_get proceeds with whatever active_model it was given",
      "evidence": "assert self._context.get(\"active_model\") == \"account.cash.deposit\""
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OCA",
      "path": "account-financial-tools/account_dashboard_banner/models/account_dashboard_banner_cell.py",
      "line": 289,
      "verdict": "false",
      "class": "internal-invariant",
      "why": "sign is set by this module a few lines up and can only be 1 or -1 unless the module is broken",
      "evidence": "assert sign in (1, -1)"
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OCA",
      "path": "account-financial-tools/account_move_template/wizard/account_move_template_run.py",
      "line": 117,
      "verdict": "true",
      "class": "validates-caller-data",
      "why": "self.overwrite is a text field a user types; the assert is the type check on literal_eval's result, and the except clause catches AssertionError explicitly -- this IS the validation, and under -O it disappears and .keys() raises AttributeError instead of the ValidationError the code means to raise",
      "evidence": "assert isinstance(overwrite_vals, dict)"
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OCA",
      "path": "server-tools/base_time_parameter/models/base.py",
      "line": 19,
      "verdict": "true",
      "class": "validates-caller-data",
      "why": "get_time_parameter is public and date arrives from the caller, possibly resolved off a field by name; under -O a wrong type reaches the search",
      "evidence": "assert type(date) is datetime.date or date is None, \"Wrong date\""
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OCA",
      "path": "server-tools/jsonifier/models/ir_exports_resolver.py",
      "line": 42,
      "verdict": "false",
      "class": "re-enforced-on-the-next-line",
      "why": "the assert guards a zip(..., strict=True) one line below, which raises ValueError on the same mismatch; removing the assert changes the exception type and nothing else",
      "evidence": "assert len(param) == len(records)"
    },
    {
      "rule": "python.assert-is-not-validation",
      "tree": "OCA",
      "path": "server-tools/jsonifier/models/utils.py",
      "line": 3,
      "verdict": "false",
      "class": "internal-invariant",
      "why": "a module-private helper's contract with its only callers",
      "evidence": "assert isinstance(parser, list)"
    },
    {
      "rule": "python.decimal-for-money-not-float",
      "tree": "OCA",
      "path": "account-financial-tools/account_payroll_sheet_import/wizard/payroll_import_wizard.py",
      "line": 110,
      "verdict": "false",
      "class": "odoo-monetary-is-float",
      "why": "a spreadsheet cell becomes an accounting amount on a Monetary field, which the ORM defines as float",
      "evidence": "amount = float(raw_value)"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OCA",
      "path": "account-financial-tools/account_fiscal_year_auto_create/models/account_fiscal_year.py",
      "line": 23,
      "verdict": "false",
      "class": "created-a-day-ahead",
      "why": "the cron creates the next fiscal year while the last still has a day to run (date_to < today + 1 day), which is further ahead than any offset between UTC and the company's calendar. Re-read 2026-09-26; was classified true.",
      "evidence": "last_fiscal_year.date_to < datetime.now().date() + relativedelta(days=1)",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OCA",
      "path": "account-financial-tools/purchase_unreconciled/models/account_move_line.py",
      "line": 45,
      "verdict": "true",
      "class": "server-date-as-a-user-date",
      "why": "an accounting move's date defaults to the server's UTC date. For a company ahead of UTC, a write-off made in its early hours is dated the day before, and at month end in the period before. fields.Date.context_today is the idiom. Stands after the 2026-09-26 re-read, under a class that does not depend on the server's clock.",
      "evidence": "move_date = writeoff_vals.get(\"date\", datetime.now())"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OCA",
      "path": "server-tools/auditlog/models/autovacuum.py",
      "line": 25,
      "verdict": "false",
      "class": "utc-by-odoo",
      "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.",
      "evidence": "deadline = datetime.now() - timedelta(days=days)",
      "was": "true"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OCA",
      "path": "server-tools/auto_backup/models/db_backup.py",
      "line": 146,
      "verdict": "false",
      "class": "display-only",
      "why": "the timestamp names a backup file",
      "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OCA",
      "path": "server-tools/auto_backup/models/db_backup.py",
      "line": 171,
      "verdict": "false",
      "class": "display-only",
      "why": "the same, on the sftp path",
      "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OCA",
      "path": "server-tools/auto_backup/models/db_backup.py",
      "line": 218,
      "verdict": "false",
      "class": "self-consistent-local",
      "why": "cleanup() builds the oldest filename from the same local clock the filenames were written with, so the two agree",
      "evidence": "now = datetime.now()"
    },
    {
      "rule": "python.naive-and-aware-datetimes-do-not-compare",
      "tree": "OCA",
      "path": "server-tools/excel_import_export/models/xlsx_export.py",
      "line": 268,
      "verdict": "false",
      "class": "display-only",
      "why": "a temp filename uniquifier",
      "evidence": "stamp = dt.utcnow().strftime(\"%H%M%S%f\")[:-3]"
    },
    {
      "rule": "python.path-join-does-not-contain-a-path",
      "tree": "OCA",
      "path": "server-tools/auto_backup/models/db_backup.py",
      "line": 154,
      "verdict": "false",
      "class": "no-user-supplied-component",
      "why": "rec.folder is configuration and filename is generated from a timestamp; neither is a path a caller can traverse with",
      "evidence": "with open(os.path.join(rec.folder, filename), \"wb\") as destiny:"
    },
    {
      "rule": "python.path-join-does-not-contain-a-path",
      "tree": "OCA",
      "path": "server-tools/auto_backup/models/db_backup.py",
      "line": 186,
      "verdict": "false",
      "class": "no-user-supplied-component",
      "why": "the same join on the sftp path",
      "evidence": "os.path.join(rec.folder, filename), \"wb\""
    },
    {
      "rule": "python.path-join-does-not-contain-a-path",
      "tree": "OCA",
      "path": "server-tools/upgrade_analysis/models/upgrade_analysis.py",
      "line": 106,
      "verdict": "false",
      "class": "no-user-supplied-component",
      "why": "full_path is built from get_module_path and filename is internal",
      "evidence": "logfile = os.path.join(full_path, filename)"
    }
  ],
  "note": "Every finding the composition ADDS to an Odoo review, read by hand in its own file. These 8 rules were previously unreachable from the odoo stack: selection was strict equality on the stack field, so an Odoo module got the 19 ORM rules and none of these.\n\nThe verdict test for the datetime rule, which is 32 of the 50: an Odoo Datetime field is naive UTC by contract, so a naive LOCAL datetime that reaches one -- written to it, compared against it, or handed to something that does -- is a true positive, and one that is only formatted for display or used to name a file is not. Note what that makes these: latent on a server clocked to UTC, wrong by the offset on any other. OpenSPP's own history carries two fixes of exactly this class, which is why it is graded true rather than theoretical.\n\nThree rules fired and were never right: decimal-for-money-not-float (0 of 3), path-join-does-not-contain-a-path (0 of 4), and eval-and-pickle-execute-their-input (0 of 1). The first has a reason rather than a sample size behind it -- Odoo's Monetary field IS a float, so the rule's remedy is not the Odoo idiom -- and the other two have four findings between them, which is not enough to conclude anything about a checker. All three are named in the README rather than quietly dropped.\n\nThree more never fired at all on either tree: mutable-default-argument, bare-except-catches-the-exit and subprocess-shell-true-is-injection. Silence is not a pass.\n\nTwo of the 50 are the same checker defect: the pattern scanner strips # comments before matching and does not strip docstrings, so `eval()` named in prose and a `datetime.now()` inside a doctest both read as code. That is a fault in scan_source, not in either rule.\n\nOne finding is suppressed and should not be. The consent-receipt timestamp sits inside a return statement that carries a `# nosemgrep: odoo-sudo-without-context` marker four lines below it, and suppression reads any marker in the statement without asking which rule it names. It is counted apart here, as fieldtest counts it.\n\nSince 210b09e a marker with words in its code covers only rules sharing one of those words, so a sudo marker no longer speaks for this datetime finding: it is reported, and the totals above now count it with the rest. 27 true of 51, 21 of them the datetime rule.\n\n2026-09-26: 18 of the datetime rule's 21 true verdicts reversed. They rested on a naive LOCAL datetime differing from the ORM's naive UTC by the server's offset, and an Odoo process has no offset: import odoo sets TZ=UTC, and fields.Datetime.now() is datetime.now() itself. Three stand: two naive UTC timestamps leaving through an API with no offset, and one server date used as a company's calendar day. Each reversed row keeps was: true."
}