trueppm-api 0.4.0__py3-none-any.whl

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.
Files changed (799) hide show
  1. trueppm_api/__init__.py +27 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +346 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
  24. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  25. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  26. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  27. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  28. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
  29. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  30. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  31. trueppm_api/apps/access/migrations/__init__.py +0 -0
  32. trueppm_api/apps/access/minter_role.py +38 -0
  33. trueppm_api/apps/access/models.py +588 -0
  34. trueppm_api/apps/access/permissions.py +3263 -0
  35. trueppm_api/apps/access/serializers.py +1007 -0
  36. trueppm_api/apps/access/services.py +878 -0
  37. trueppm_api/apps/access/signals.py +171 -0
  38. trueppm_api/apps/access/tasks.py +50 -0
  39. trueppm_api/apps/access/throttles.py +96 -0
  40. trueppm_api/apps/access/urls.py +191 -0
  41. trueppm_api/apps/access/views.py +1965 -0
  42. trueppm_api/apps/agents/__init__.py +6 -0
  43. trueppm_api/apps/agents/apps.py +11 -0
  44. trueppm_api/apps/agents/canonical.py +73 -0
  45. trueppm_api/apps/agents/deferred.py +108 -0
  46. trueppm_api/apps/agents/management/__init__.py +0 -0
  47. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  48. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  49. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  50. trueppm_api/apps/agents/middleware.py +44 -0
  51. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  52. trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
  53. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  54. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  55. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  56. trueppm_api/apps/agents/models.py +371 -0
  57. trueppm_api/apps/agents/refusal.py +103 -0
  58. trueppm_api/apps/agents/serializers.py +85 -0
  59. trueppm_api/apps/agents/services.py +397 -0
  60. trueppm_api/apps/agents/signals.py +30 -0
  61. trueppm_api/apps/agents/urls.py +15 -0
  62. trueppm_api/apps/agents/views.py +109 -0
  63. trueppm_api/apps/csvimport/__init__.py +12 -0
  64. trueppm_api/apps/csvimport/apps.py +11 -0
  65. trueppm_api/apps/csvimport/mapping.py +363 -0
  66. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  67. trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
  68. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  69. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  70. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  71. trueppm_api/apps/csvimport/models.py +111 -0
  72. trueppm_api/apps/csvimport/parser.py +1825 -0
  73. trueppm_api/apps/csvimport/services.py +48 -0
  74. trueppm_api/apps/csvimport/tasks.py +459 -0
  75. trueppm_api/apps/csvimport/template.py +37 -0
  76. trueppm_api/apps/csvimport/urls.py +50 -0
  77. trueppm_api/apps/csvimport/views.py +793 -0
  78. trueppm_api/apps/history/__init__.py +0 -0
  79. trueppm_api/apps/history/apps.py +12 -0
  80. trueppm_api/apps/history/changelog.py +359 -0
  81. trueppm_api/apps/history/diff_policy.py +178 -0
  82. trueppm_api/apps/history/serializers.py +104 -0
  83. trueppm_api/apps/history/signals.py +82 -0
  84. trueppm_api/apps/history/tasks.py +105 -0
  85. trueppm_api/apps/history/urls.py +38 -0
  86. trueppm_api/apps/history/views.py +443 -0
  87. trueppm_api/apps/idempotency/__init__.py +0 -0
  88. trueppm_api/apps/idempotency/apps.py +11 -0
  89. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  90. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  91. trueppm_api/apps/idempotency/mixins.py +226 -0
  92. trueppm_api/apps/idempotency/models.py +75 -0
  93. trueppm_api/apps/idempotency/tasks.py +61 -0
  94. trueppm_api/apps/integrations/__init__.py +13 -0
  95. trueppm_api/apps/integrations/apps.py +59 -0
  96. trueppm_api/apps/integrations/connections.py +806 -0
  97. trueppm_api/apps/integrations/encryption.py +92 -0
  98. trueppm_api/apps/integrations/external_sources.py +863 -0
  99. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  100. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  101. trueppm_api/apps/integrations/http.py +597 -0
  102. trueppm_api/apps/integrations/me_work.py +218 -0
  103. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  104. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  105. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  106. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  107. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  108. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  109. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  110. trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
  111. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  112. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  113. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  114. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  115. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  116. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  117. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  118. trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
  119. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  120. trueppm_api/apps/integrations/models.py +635 -0
  121. trueppm_api/apps/integrations/notification_channels.py +57 -0
  122. trueppm_api/apps/integrations/opengraph.py +229 -0
  123. trueppm_api/apps/integrations/outgoing.py +201 -0
  124. trueppm_api/apps/integrations/providers.py +528 -0
  125. trueppm_api/apps/integrations/registry.py +345 -0
  126. trueppm_api/apps/integrations/serializers.py +453 -0
  127. trueppm_api/apps/integrations/services.py +171 -0
  128. trueppm_api/apps/integrations/tasks.py +734 -0
  129. trueppm_api/apps/integrations/throttles.py +165 -0
  130. trueppm_api/apps/integrations/urls.py +77 -0
  131. trueppm_api/apps/integrations/views.py +1158 -0
  132. trueppm_api/apps/jiraimport/__init__.py +8 -0
  133. trueppm_api/apps/jiraimport/apps.py +11 -0
  134. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  135. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  136. trueppm_api/apps/jiraimport/models.py +70 -0
  137. trueppm_api/apps/jiraimport/parser.py +350 -0
  138. trueppm_api/apps/jiraimport/services.py +46 -0
  139. trueppm_api/apps/jiraimport/tasks.py +280 -0
  140. trueppm_api/apps/jiraimport/urls.py +15 -0
  141. trueppm_api/apps/jiraimport/views.py +153 -0
  142. trueppm_api/apps/msproject/__init__.py +0 -0
  143. trueppm_api/apps/msproject/apps.py +11 -0
  144. trueppm_api/apps/msproject/dataclasses.py +153 -0
  145. trueppm_api/apps/msproject/exporter.py +551 -0
  146. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  147. trueppm_api/apps/msproject/importer.py +1095 -0
  148. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  149. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  150. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  151. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  152. trueppm_api/apps/msproject/models.py +81 -0
  153. trueppm_api/apps/msproject/parser.py +1276 -0
  154. trueppm_api/apps/msproject/serializers.py +136 -0
  155. trueppm_api/apps/msproject/services.py +58 -0
  156. trueppm_api/apps/msproject/tasks.py +439 -0
  157. trueppm_api/apps/msproject/urls.py +39 -0
  158. trueppm_api/apps/msproject/views.py +531 -0
  159. trueppm_api/apps/notifications/__init__.py +7 -0
  160. trueppm_api/apps/notifications/apps.py +15 -0
  161. trueppm_api/apps/notifications/backfill.py +79 -0
  162. trueppm_api/apps/notifications/categories.py +112 -0
  163. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  164. trueppm_api/apps/notifications/digests.py +462 -0
  165. trueppm_api/apps/notifications/email_backend.py +350 -0
  166. trueppm_api/apps/notifications/email_health.py +128 -0
  167. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  168. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
  169. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  170. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  171. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  172. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  173. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  174. trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
  175. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  176. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  177. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  178. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  179. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  180. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  181. trueppm_api/apps/notifications/models.py +1148 -0
  182. trueppm_api/apps/notifications/receivers.py +155 -0
  183. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  184. trueppm_api/apps/notifications/serializers.py +1094 -0
  185. trueppm_api/apps/notifications/services.py +1489 -0
  186. trueppm_api/apps/notifications/tasks.py +762 -0
  187. trueppm_api/apps/notifications/throttles.py +98 -0
  188. trueppm_api/apps/notifications/urls.py +97 -0
  189. trueppm_api/apps/notifications/views.py +919 -0
  190. trueppm_api/apps/observability/__init__.py +0 -0
  191. trueppm_api/apps/observability/apps.py +31 -0
  192. trueppm_api/apps/observability/logging.py +213 -0
  193. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  194. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  195. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  196. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  197. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  198. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  199. trueppm_api/apps/observability/models.py +141 -0
  200. trueppm_api/apps/observability/otel/__init__.py +45 -0
  201. trueppm_api/apps/observability/otel/attributes.py +247 -0
  202. trueppm_api/apps/observability/otel/export_health.py +533 -0
  203. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  204. trueppm_api/apps/observability/otel/metrics.py +566 -0
  205. trueppm_api/apps/observability/otel/provider.py +511 -0
  206. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  207. trueppm_api/apps/observability/purge_registry.py +124 -0
  208. trueppm_api/apps/observability/retention.py +139 -0
  209. trueppm_api/apps/observability/selectors.py +993 -0
  210. trueppm_api/apps/observability/serializers.py +142 -0
  211. trueppm_api/apps/observability/services.py +407 -0
  212. trueppm_api/apps/observability/tasks.py +266 -0
  213. trueppm_api/apps/observability/urls.py +27 -0
  214. trueppm_api/apps/observability/views.py +537 -0
  215. trueppm_api/apps/profiles/__init__.py +0 -0
  216. trueppm_api/apps/profiles/apps.py +15 -0
  217. trueppm_api/apps/profiles/constants.py +54 -0
  218. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  219. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  220. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  221. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  222. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  223. trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
  224. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  225. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  226. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  227. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  228. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  229. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  230. trueppm_api/apps/profiles/models.py +276 -0
  231. trueppm_api/apps/profiles/serializers.py +199 -0
  232. trueppm_api/apps/profiles/services.py +541 -0
  233. trueppm_api/apps/profiles/views.py +124 -0
  234. trueppm_api/apps/projects/__init__.py +0 -0
  235. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  236. trueppm_api/apps/projects/amend.py +133 -0
  237. trueppm_api/apps/projects/apps.py +38 -0
  238. trueppm_api/apps/projects/asset_feed.py +459 -0
  239. trueppm_api/apps/projects/asset_views.py +317 -0
  240. trueppm_api/apps/projects/attachment_policy.py +278 -0
  241. trueppm_api/apps/projects/authentication.py +458 -0
  242. trueppm_api/apps/projects/backfill.py +459 -0
  243. trueppm_api/apps/projects/backlog_services.py +324 -0
  244. trueppm_api/apps/projects/backlog_views.py +302 -0
  245. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  246. trueppm_api/apps/projects/blocker_services.py +510 -0
  247. trueppm_api/apps/projects/board_activity.py +462 -0
  248. trueppm_api/apps/projects/board_activity_views.py +186 -0
  249. trueppm_api/apps/projects/board_lanes.py +134 -0
  250. trueppm_api/apps/projects/bulk_settings.py +194 -0
  251. trueppm_api/apps/projects/bundled_templates.py +215 -0
  252. trueppm_api/apps/projects/calendar_settings.py +180 -0
  253. trueppm_api/apps/projects/ceremony_views.py +244 -0
  254. trueppm_api/apps/projects/commit_moment.py +212 -0
  255. trueppm_api/apps/projects/config_notice.py +1330 -0
  256. trueppm_api/apps/projects/custom_field_values.py +266 -0
  257. trueppm_api/apps/projects/decisions_services.py +132 -0
  258. trueppm_api/apps/projects/decisions_views.py +97 -0
  259. trueppm_api/apps/projects/estimation_scale.py +126 -0
  260. trueppm_api/apps/projects/export_bundle.py +423 -0
  261. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
  262. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
  263. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
  264. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
  265. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
  266. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  267. trueppm_api/apps/projects/inbound_sync.py +555 -0
  268. trueppm_api/apps/projects/iteration_label.py +85 -0
  269. trueppm_api/apps/projects/key_views.py +468 -0
  270. trueppm_api/apps/projects/keys.py +205 -0
  271. trueppm_api/apps/projects/lifecycle.py +191 -0
  272. trueppm_api/apps/projects/management/__init__.py +0 -0
  273. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  274. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  275. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  276. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  277. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  278. trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
  279. trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
  280. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
  281. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  282. trueppm_api/apps/projects/mcp_settings.py +305 -0
  283. trueppm_api/apps/projects/methodology.py +178 -0
  284. trueppm_api/apps/projects/migration_helpers.py +75 -0
  285. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  286. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
  287. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  288. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  289. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  290. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  291. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  292. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  293. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  294. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  295. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  296. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  297. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  298. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  299. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  300. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  301. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  302. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  303. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  304. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  305. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  306. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  307. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  308. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  309. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  310. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  311. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  312. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  313. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  314. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  315. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  316. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  317. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  318. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  319. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  320. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  321. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  322. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  323. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  324. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  325. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  326. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  327. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  328. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  329. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  330. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  331. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  332. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  333. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  334. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  335. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  336. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  337. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  338. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  339. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  340. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  341. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  342. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  343. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  344. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  345. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  346. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  347. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  348. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  349. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  350. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  351. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  352. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  353. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  354. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  355. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  356. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  357. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  358. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  359. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  360. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  361. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  362. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  363. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  364. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  365. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  366. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  367. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  368. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  369. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  370. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  371. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  372. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  373. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  374. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  375. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  376. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  377. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  378. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  379. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
  380. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  381. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  382. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  383. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  384. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  385. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  386. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  387. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  388. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  389. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  390. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
  391. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  392. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  393. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  394. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  395. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  396. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
  397. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  398. trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
  399. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  400. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  401. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  402. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  403. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  404. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  405. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  406. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  407. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  408. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  409. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  410. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  411. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  412. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  413. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  414. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  415. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  416. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  417. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  418. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  419. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  420. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  421. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  422. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  423. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  424. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  425. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  426. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  427. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  428. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  429. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  430. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  431. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  432. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  433. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  434. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  435. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  436. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  437. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  438. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  439. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  440. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  441. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  442. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  443. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  444. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  445. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  446. trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
  447. trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
  448. trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
  449. trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
  450. trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
  451. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  452. trueppm_api/apps/projects/models.py +9209 -0
  453. trueppm_api/apps/projects/poker_services.py +154 -0
  454. trueppm_api/apps/projects/poker_views.py +398 -0
  455. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  456. trueppm_api/apps/projects/program_label_services.py +94 -0
  457. trueppm_api/apps/projects/program_label_views.py +189 -0
  458. trueppm_api/apps/projects/program_rollup.py +903 -0
  459. trueppm_api/apps/projects/program_schedule.py +767 -0
  460. trueppm_api/apps/projects/program_views.py +3445 -0
  461. trueppm_api/apps/projects/project_templates.py +422 -0
  462. trueppm_api/apps/projects/receivers.py +234 -0
  463. trueppm_api/apps/projects/refusal_codes.py +140 -0
  464. trueppm_api/apps/projects/reorder_services.py +145 -0
  465. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  466. trueppm_api/apps/projects/retro_board_services.py +368 -0
  467. trueppm_api/apps/projects/retro_services.py +285 -0
  468. trueppm_api/apps/projects/risk_import.py +396 -0
  469. trueppm_api/apps/projects/row_identity.py +113 -0
  470. trueppm_api/apps/projects/schema_migrations.py +228 -0
  471. trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
  472. trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
  473. trueppm_api/apps/projects/seed/__init__.py +61 -0
  474. trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
  475. trueppm_api/apps/projects/seed/exporter.py +1717 -0
  476. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  477. trueppm_api/apps/projects/seed/importer.py +2764 -0
  478. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  479. trueppm_api/apps/projects/seed/reldates.py +126 -0
  480. trueppm_api/apps/projects/seed/replace.py +128 -0
  481. trueppm_api/apps/projects/seed/replay.py +1695 -0
  482. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  483. trueppm_api/apps/projects/seed/samples.py +427 -0
  484. trueppm_api/apps/projects/seed/validation.py +1169 -0
  485. trueppm_api/apps/projects/serializers.py +12904 -0
  486. trueppm_api/apps/projects/services.py +7525 -0
  487. trueppm_api/apps/projects/share_serializers.py +142 -0
  488. trueppm_api/apps/projects/share_services.py +373 -0
  489. trueppm_api/apps/projects/share_views.py +395 -0
  490. trueppm_api/apps/projects/sharing_settings.py +187 -0
  491. trueppm_api/apps/projects/signal_privacy_services.py +910 -0
  492. trueppm_api/apps/projects/signal_privacy_views.py +485 -0
  493. trueppm_api/apps/projects/signals.py +132 -0
  494. trueppm_api/apps/projects/slip_conflict.py +288 -0
  495. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  496. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  497. trueppm_api/apps/projects/standup.py +304 -0
  498. trueppm_api/apps/projects/standup_views.py +99 -0
  499. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  500. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  501. trueppm_api/apps/projects/surface_visibility.py +126 -0
  502. trueppm_api/apps/projects/task_batch_services.py +379 -0
  503. trueppm_api/apps/projects/task_bulk.py +1244 -0
  504. trueppm_api/apps/projects/task_classification.py +561 -0
  505. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  506. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  507. trueppm_api/apps/projects/task_grouping.py +754 -0
  508. trueppm_api/apps/projects/tasks.py +1979 -0
  509. trueppm_api/apps/projects/template_divergence.py +168 -0
  510. trueppm_api/apps/projects/template_services.py +160 -0
  511. trueppm_api/apps/projects/template_tasks.py +261 -0
  512. trueppm_api/apps/projects/template_views.py +829 -0
  513. trueppm_api/apps/projects/throttles.py +311 -0
  514. trueppm_api/apps/projects/urls.py +1016 -0
  515. trueppm_api/apps/projects/utilization.py +909 -0
  516. trueppm_api/apps/projects/views.py +21225 -0
  517. trueppm_api/apps/projects/wbs_paths.py +119 -0
  518. trueppm_api/apps/resources/__init__.py +0 -0
  519. trueppm_api/apps/resources/apps.py +8 -0
  520. trueppm_api/apps/resources/capacity.py +121 -0
  521. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  522. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  523. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  524. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  525. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  526. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  527. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  528. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  529. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  530. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  531. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  532. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  533. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  534. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  535. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  536. trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
  537. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  538. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  539. trueppm_api/apps/resources/models.py +298 -0
  540. trueppm_api/apps/resources/serializers.py +426 -0
  541. trueppm_api/apps/resources/services.py +465 -0
  542. trueppm_api/apps/resources/urls.py +28 -0
  543. trueppm_api/apps/resources/views.py +1752 -0
  544. trueppm_api/apps/scheduling/__init__.py +0 -0
  545. trueppm_api/apps/scheduling/apps.py +153 -0
  546. trueppm_api/apps/scheduling/calendars.py +144 -0
  547. trueppm_api/apps/scheduling/deadletter.py +91 -0
  548. trueppm_api/apps/scheduling/finish_reading.py +198 -0
  549. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  550. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  551. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  552. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  553. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  554. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  555. trueppm_api/apps/scheduling/mc_admission.py +181 -0
  556. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  557. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  558. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  559. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  560. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  561. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  562. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  563. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  564. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  565. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  566. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  567. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  568. trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
  569. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  570. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  571. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  572. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  573. trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
  574. trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
  575. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  576. trueppm_api/apps/scheduling/models.py +542 -0
  577. trueppm_api/apps/scheduling/receivers.py +160 -0
  578. trueppm_api/apps/scheduling/risk_premium.py +334 -0
  579. trueppm_api/apps/scheduling/serializers.py +723 -0
  580. trueppm_api/apps/scheduling/services.py +1251 -0
  581. trueppm_api/apps/scheduling/signals.py +56 -0
  582. trueppm_api/apps/scheduling/tasks.py +2248 -0
  583. trueppm_api/apps/scheduling/telemetry.py +80 -0
  584. trueppm_api/apps/scheduling/units.py +35 -0
  585. trueppm_api/apps/scheduling/urls.py +57 -0
  586. trueppm_api/apps/scheduling/views.py +2908 -0
  587. trueppm_api/apps/sso/__init__.py +0 -0
  588. trueppm_api/apps/sso/apps.py +19 -0
  589. trueppm_api/apps/sso/extensions.py +130 -0
  590. trueppm_api/apps/sso/management/__init__.py +0 -0
  591. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  592. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  593. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  594. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  595. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  596. trueppm_api/apps/sso/models.py +148 -0
  597. trueppm_api/apps/sso/serializers.py +446 -0
  598. trueppm_api/apps/sso/services.py +1473 -0
  599. trueppm_api/apps/sso/urls.py +47 -0
  600. trueppm_api/apps/sso/views.py +935 -0
  601. trueppm_api/apps/sync/__init__.py +0 -0
  602. trueppm_api/apps/sync/apps.py +15 -0
  603. trueppm_api/apps/sync/backfill.py +208 -0
  604. trueppm_api/apps/sync/broadcast.py +369 -0
  605. trueppm_api/apps/sync/conflict.py +262 -0
  606. trueppm_api/apps/sync/consumers.py +548 -0
  607. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  608. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  609. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  610. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  611. trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
  612. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  613. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  614. trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
  615. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  616. trueppm_api/apps/sync/models.py +272 -0
  617. trueppm_api/apps/sync/pagination.py +231 -0
  618. trueppm_api/apps/sync/sequence.py +488 -0
  619. trueppm_api/apps/sync/serializers.py +688 -0
  620. trueppm_api/apps/sync/tasks.py +260 -0
  621. trueppm_api/apps/sync/throttles.py +175 -0
  622. trueppm_api/apps/sync/upload.py +556 -0
  623. trueppm_api/apps/sync/urls.py +17 -0
  624. trueppm_api/apps/sync/views.py +957 -0
  625. trueppm_api/apps/sync/ws_auth.py +193 -0
  626. trueppm_api/apps/taskruns/__init__.py +0 -0
  627. trueppm_api/apps/taskruns/apps.py +11 -0
  628. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  629. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  630. trueppm_api/apps/taskruns/models.py +68 -0
  631. trueppm_api/apps/taskruns/serializers.py +92 -0
  632. trueppm_api/apps/taskruns/tasks.py +60 -0
  633. trueppm_api/apps/taskruns/tracker.py +286 -0
  634. trueppm_api/apps/taskruns/urls.py +46 -0
  635. trueppm_api/apps/taskruns/views.py +195 -0
  636. trueppm_api/apps/teams/__init__.py +8 -0
  637. trueppm_api/apps/teams/apps.py +16 -0
  638. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  639. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
  640. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  641. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  642. trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
  643. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  644. trueppm_api/apps/teams/models.py +145 -0
  645. trueppm_api/apps/teams/permissions.py +149 -0
  646. trueppm_api/apps/teams/serializers.py +95 -0
  647. trueppm_api/apps/teams/services.py +440 -0
  648. trueppm_api/apps/teams/signals.py +61 -0
  649. trueppm_api/apps/teams/urls.py +34 -0
  650. trueppm_api/apps/teams/views.py +201 -0
  651. trueppm_api/apps/timetracking/__init__.py +0 -0
  652. trueppm_api/apps/timetracking/apps.py +9 -0
  653. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  654. trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
  655. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  656. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  657. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  658. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  659. trueppm_api/apps/timetracking/models.py +178 -0
  660. trueppm_api/apps/timetracking/serializers.py +175 -0
  661. trueppm_api/apps/timetracking/services.py +211 -0
  662. trueppm_api/apps/timetracking/urls.py +47 -0
  663. trueppm_api/apps/timetracking/views.py +597 -0
  664. trueppm_api/apps/webhooks/__init__.py +0 -0
  665. trueppm_api/apps/webhooks/apps.py +8 -0
  666. trueppm_api/apps/webhooks/backfill.py +90 -0
  667. trueppm_api/apps/webhooks/dispatch.py +279 -0
  668. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  669. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
  670. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  671. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  672. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  673. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  674. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  675. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  676. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  677. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
  678. trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
  679. trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
  680. trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
  681. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  682. trueppm_api/apps/webhooks/models.py +437 -0
  683. trueppm_api/apps/webhooks/serializers.py +326 -0
  684. trueppm_api/apps/webhooks/tasks.py +680 -0
  685. trueppm_api/apps/webhooks/urls.py +22 -0
  686. trueppm_api/apps/webhooks/views.py +533 -0
  687. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  688. trueppm_api/apps/workflow_engine/apps.py +23 -0
  689. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  690. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  691. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  692. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  693. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  694. trueppm_api/apps/workflow_engine/models.py +265 -0
  695. trueppm_api/apps/workflow_engine/tasks.py +371 -0
  696. trueppm_api/apps/workspace/__init__.py +0 -0
  697. trueppm_api/apps/workspace/apps.py +10 -0
  698. trueppm_api/apps/workspace/backfill.py +111 -0
  699. trueppm_api/apps/workspace/export.py +254 -0
  700. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  701. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  702. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  703. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  704. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  705. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  706. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  707. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  708. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  709. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  710. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  711. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  712. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  713. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  714. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  715. trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
  716. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  717. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  718. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  719. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  720. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  721. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  722. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  723. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  724. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  725. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  726. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  727. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  728. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  729. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  730. trueppm_api/apps/workspace/models.py +857 -0
  731. trueppm_api/apps/workspace/permissions.py +187 -0
  732. trueppm_api/apps/workspace/serializers.py +590 -0
  733. trueppm_api/apps/workspace/services.py +967 -0
  734. trueppm_api/apps/workspace/signals.py +37 -0
  735. trueppm_api/apps/workspace/tasks.py +616 -0
  736. trueppm_api/apps/workspace/urls.py +113 -0
  737. trueppm_api/apps/workspace/views.py +1581 -0
  738. trueppm_api/asgi.py +49 -0
  739. trueppm_api/celery.py +40 -0
  740. trueppm_api/core/__init__.py +0 -0
  741. trueppm_api/core/admin_site.py +206 -0
  742. trueppm_api/core/auth_views.py +934 -0
  743. trueppm_api/core/constant_time.py +51 -0
  744. trueppm_api/core/csp.py +54 -0
  745. trueppm_api/core/db.py +87 -0
  746. trueppm_api/core/db_session.py +37 -0
  747. trueppm_api/core/demo_read_only.py +314 -0
  748. trueppm_api/core/exception_handlers.py +157 -0
  749. trueppm_api/core/export_downloads.py +56 -0
  750. trueppm_api/core/extension_providers.py +56 -0
  751. trueppm_api/core/extension_signals.py +147 -0
  752. trueppm_api/core/idempotent.py +260 -0
  753. trueppm_api/core/metadata.py +42 -0
  754. trueppm_api/core/middleware.py +51 -0
  755. trueppm_api/core/openapi.py +1221 -0
  756. trueppm_api/core/password_reset.py +513 -0
  757. trueppm_api/core/protect_conflict.py +100 -0
  758. trueppm_api/core/ratelimit.py +148 -0
  759. trueppm_api/core/redis_throttle.py +82 -0
  760. trueppm_api/core/refresh_cookie_policy.py +156 -0
  761. trueppm_api/core/request_body.py +86 -0
  762. trueppm_api/core/security_checks.py +1034 -0
  763. trueppm_api/core/storage_config.py +105 -0
  764. trueppm_api/core/text_decode.py +168 -0
  765. trueppm_api/core/throttling.py +514 -0
  766. trueppm_api/core/valkey.py +214 -0
  767. trueppm_api/core/valkey_cache.py +66 -0
  768. trueppm_api/core/valkey_checks.py +104 -0
  769. trueppm_api/core/valkey_config.py +39 -0
  770. trueppm_api/core/worker_broker_wait.py +149 -0
  771. trueppm_api/core/worker_heartbeat.py +134 -0
  772. trueppm_api/core/worker_selfheal.py +205 -0
  773. trueppm_api/fields.py +36 -0
  774. trueppm_api/permissions.py +30 -0
  775. trueppm_api/py.typed +0 -0
  776. trueppm_api/routing.py +11 -0
  777. trueppm_api/settings/__init__.py +0 -0
  778. trueppm_api/settings/base.py +2605 -0
  779. trueppm_api/settings/dev.py +296 -0
  780. trueppm_api/settings/prod.py +265 -0
  781. trueppm_api/urls.py +217 -0
  782. trueppm_api/workflows/__init__.py +38 -0
  783. trueppm_api/workflows/backends/__init__.py +6 -0
  784. trueppm_api/workflows/backends/default.py +297 -0
  785. trueppm_api/workflows/consumers/__init__.py +11 -0
  786. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  787. trueppm_api/workflows/interface.py +148 -0
  788. trueppm_api/workflows/registry.py +106 -0
  789. trueppm_api/workflows/services.py +90 -0
  790. trueppm_api/wsgi.py +11 -0
  791. trueppm_api-0.4.0.dist-info/METADATA +273 -0
  792. trueppm_api-0.4.0.dist-info/RECORD +799 -0
  793. trueppm_api-0.4.0.dist-info/WHEEL +4 -0
  794. trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
  795. trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
  796. trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
  797. trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
  798. trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
  799. trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
@@ -0,0 +1,903 @@
1
+ """Program rollup-KPI consumer (ADR-0088, #713).
2
+
3
+ Reads the per-program rollup config shipped by #527 (``rollup_enabled_kpis`` +
4
+ ``rollup_aggregation_policy`` on :class:`Program`) and computes the actual
5
+ rolled-up KPI values across the program's *own* projects. Intra-program only —
6
+ cross-program aggregation is Enterprise (ADR-0070).
7
+
8
+ Design (ADR-0088):
9
+
10
+ - **7 KPIs are computable today.** ``cost_variance`` / ``budget_utilization`` are
11
+ deferred to the cost/EVM model (#754) and ``p80_completion`` to a persistent
12
+ Monte Carlo store (#753); those return ``{"available": False, "reason": ...}``
13
+ so the UI can explain *why* a toggled KPI shows nothing rather than fabricating
14
+ a zero.
15
+ - **The aggregation policy applies where it is meaningful.** ``worst`` / ``average``
16
+ / ``task_weighted`` govern the health bands (``schedule_health``,
17
+ ``milestone_health``), the day-variances (``baseline_variance``,
18
+ ``schedule_variance``), and the headline program health dot. Counts
19
+ (``critical_tasks``, ``at_risk_tasks``) and risk exposure (``risk_score``) roll
20
+ up as program **totals** regardless of policy — the sum is the only PM-useful
21
+ number for an additive metric.
22
+ - ``weighted_by_budget`` has no weight to use until #754, so it falls back to
23
+ ``average`` and the response flags ``policy_available: False``; the rollup is
24
+ never blanked just because the chosen policy cannot be honored.
25
+
26
+ Implemented with a fixed, small number of grouped (``values().annotate()``)
27
+ queries across the program's projects — never a per-project ``ProjectOverviewView``
28
+ call — to hold the ≤200 ms p95 budget (ADR-0030/0088). Pure read; no writes, no
29
+ async side effects (ADR-0088 Durable Execution: all N/A).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import datetime
35
+ from collections.abc import Collection
36
+ from typing import Any
37
+
38
+ from django.db.models import BooleanField, Count, F, Max, Q, Sum
39
+ from django.db.models.expressions import RawSQL
40
+ from django.utils import timezone
41
+
42
+ from trueppm_api.apps.projects.models import (
43
+ AggregationPolicy,
44
+ Baseline,
45
+ BaselineTask,
46
+ Program,
47
+ Project,
48
+ Risk,
49
+ RiskStatus,
50
+ RollupKpi,
51
+ Task,
52
+ TaskStatus,
53
+ )
54
+
55
+ # KPIs without a per-project source yet (ADR-0088). The reason string is stable
56
+ # so the web layer and #673's preview can branch on it.
57
+ #
58
+ # This map is the single source of truth for "can this KPI ever produce a value
59
+ # today?" and is published on the rollup-config endpoint as ``unavailable_kpis``
60
+ # (#2404). It has to be a *server* fact rather than a hard-coded client list:
61
+ # the settings picker offered all ten toggles as if they were live, so enabling
62
+ # one of these three saved a config that could never render — a control that
63
+ # lies. When #753 (Monte Carlo store) and #754 (cost/EVM model) land, deleting
64
+ # the corresponding entry here is the only change needed to light the toggle up.
65
+ DEFERRED_KPI_REASONS = {
66
+ RollupKpi.COST_VARIANCE.value: "no_cost_data",
67
+ RollupKpi.BUDGET_UTILIZATION.value: "no_cost_data",
68
+ RollupKpi.P80_COMPLETION.value: "no_montecarlo_store",
69
+ }
70
+
71
+ # Active task statuses (exclude COMPLETE) — "open work" for count KPIs. Mirrors
72
+ # the at-risk/critical definitions in ProjectViewSet.status_summary so the
73
+ # program rollup and the per-project StatusBar agree.
74
+ _ACTIVE_EXCLUDE = {TaskStatus.COMPLETE}
75
+
76
+ # Health band → ordinal for reducing (higher = healthier). ``unknown`` has no
77
+ # ordinal: it is excluded from the reduce rather than dragging the result down,
78
+ # which would punish projects that simply have no baseline yet.
79
+ _HEALTH_ORDINAL = {"critical": 0, "at_risk": 1, "on_track": 2}
80
+ _ORDINAL_TO_HEALTH = {0: "critical", 1: "at_risk", 2: "on_track"}
81
+
82
+
83
+ def compute_program_rollup(
84
+ program: Program,
85
+ *,
86
+ exclude_project_ids: Collection[Any] | None = None,
87
+ ) -> dict[str, Any]:
88
+ """Compute the rolled-up KPI block for a program's overview (ADR-0088).
89
+
90
+ Returns a dict with the program health dot, the active aggregation policy
91
+ (and whether it could be honored), the contributing project count, and a
92
+ ``kpis`` map keyed by the program's enabled :class:`RollupKpi` values. Each
93
+ entry is either ``{"available": True, "value": ..., ["unit": ...]}`` (value
94
+ may be ``None`` when a built KPI has no data yet, e.g. no active baseline) or
95
+ ``{"available": False, "reason": ...}`` for a KPI whose per-project source is
96
+ not built yet (#753/#754).
97
+
98
+ Pure read: deterministic for a given DB state, no writes, safe to call on
99
+ every GET.
100
+ """
101
+ today = timezone.localdate()
102
+ policy = program.rollup_aggregation_policy or AggregationPolicy.WORST.value
103
+ enabled = list(program.rollup_enabled_kpis or [])
104
+
105
+ # Drafts are excluded (#2962): a plan nobody has committed to must not move a
106
+ # number a PMO puts in front of a CEO. `visible_projects` is the single
107
+ # definition of that exclusion — see lifecycle.py for why it is one helper
108
+ # and not a filter repeated per surface.
109
+ from trueppm_api.apps.projects.lifecycle import visible_projects
110
+
111
+ project_qs = visible_projects(Project.objects.filter(program=program, is_deleted=False))
112
+ if exclude_project_ids is not None:
113
+ # ADR-0678 (#2482): a project that opted out of agent reads does not feed the
114
+ # rolled-up KPIs an agent sees. ``contributing_project_count`` derives from this
115
+ # set, so the response stays internally consistent — the agent is told how many
116
+ # projects contributed, never that any were withheld.
117
+ project_qs = project_qs.exclude(pk__in=exclude_project_ids)
118
+ project_ids = list(project_qs.values_list("id", flat=True))
119
+
120
+ # weighted_by_budget cannot be honored until the cost model (#754) lands, so
121
+ # it degrades to AVERAGE and the caller flags policy_available=False.
122
+ effective_policy = policy
123
+ policy_available = True
124
+ if policy == AggregationPolicy.WEIGHTED_BY_BUDGET.value:
125
+ effective_policy = AggregationPolicy.AVERAGE.value
126
+ policy_available = False
127
+
128
+ # Per-project committed task counts — the weight for task_weighted. Computed
129
+ # once and shared by every policy-governed KPI. Excludes BACKLOG (uncommitted)
130
+ # so a large grooming backlog does not skew the weight.
131
+ task_weights = _committed_task_counts(project_ids)
132
+
133
+ # schedule_health per project is always needed: it is both a KPI and the
134
+ # source of the headline program health dot (drives Program.health=AUTO).
135
+ schedule_health_by_project = _schedule_health_by_project(project_ids, today)
136
+ program_health = _reduce_health(
137
+ list(schedule_health_by_project.values()), effective_policy, task_weights, project_ids
138
+ )
139
+
140
+ kpis: dict[str, dict[str, Any]] = {}
141
+ for kpi in enabled:
142
+ if kpi in DEFERRED_KPI_REASONS:
143
+ kpis[kpi] = {"available": False, "reason": DEFERRED_KPI_REASONS[kpi]}
144
+ elif kpi == RollupKpi.SCHEDULE_HEALTH.value:
145
+ kpis[kpi] = {
146
+ "available": True,
147
+ "value": _reduce_health(
148
+ list(schedule_health_by_project.values()),
149
+ effective_policy,
150
+ task_weights,
151
+ project_ids,
152
+ ),
153
+ }
154
+ elif kpi == RollupKpi.MILESTONE_HEALTH.value:
155
+ bands = list(_milestone_health_by_project(project_ids, today).values())
156
+ kpis[kpi] = {
157
+ "available": True,
158
+ "value": _reduce_health(bands, effective_policy, task_weights, project_ids),
159
+ }
160
+ elif kpi == RollupKpi.CRITICAL_TASKS.value:
161
+ kpis[kpi] = {"available": True, "value": _critical_task_total(project_ids)}
162
+ elif kpi == RollupKpi.AT_RISK_TASKS.value:
163
+ kpis[kpi] = {"available": True, "value": _at_risk_task_total(project_ids)}
164
+ elif kpi == RollupKpi.RISK_SCORE.value:
165
+ kpis[kpi] = {"available": True, "value": _risk_score_total(project_ids)}
166
+ elif kpi == RollupKpi.BASELINE_VARIANCE.value:
167
+ kpis[kpi] = {
168
+ "available": True,
169
+ "unit": "calendar_days",
170
+ "value": _reduce_variance(
171
+ _baseline_variance_by_project(project_ids),
172
+ effective_policy,
173
+ task_weights,
174
+ ),
175
+ }
176
+ elif kpi == RollupKpi.SCHEDULE_VARIANCE.value:
177
+ kpis[kpi] = {
178
+ "available": True,
179
+ "unit": "calendar_days",
180
+ "value": _reduce_variance(
181
+ _schedule_variance_by_project(project_ids),
182
+ effective_policy,
183
+ task_weights,
184
+ ),
185
+ }
186
+
187
+ return {
188
+ "aggregation_policy": policy,
189
+ "policy_available": policy_available,
190
+ "project_count": len(project_ids),
191
+ "program_health": program_health,
192
+ "kpis": kpis,
193
+ }
194
+
195
+
196
+ def top_contributing_project(program: Program) -> dict[str, Any] | None:
197
+ """Return the project dragging ``program``'s health down, or ``None``.
198
+
199
+ "Top contributing" = worst :func:`_schedule_health_by_project` band, tie-broken
200
+ by the larger count of open critical-path tasks, then by name for stability.
201
+ Lives here rather than in the digest builder (ADR-0663) so it reuses the *same*
202
+ per-project bands the Program Overview renders — a digest that named a different
203
+ project than the screen it deep-links to would be worse than no digest at all.
204
+
205
+ Returns ``{"id", "name", "health"}`` for the worst project, or ``None`` when the
206
+ program has no projects or every project's band is ``unknown`` (no data to
207
+ attribute the program's state to).
208
+ """
209
+ today = timezone.localdate()
210
+ # Drafts are excluded (#2962/#3128) for the same reason as compute_program_rollup
211
+ # above, and one more: this function names ONE project in the weekly program-health
212
+ # email. Attributing a program's health to a plan nobody has committed to would
213
+ # deep-link a PMO into a half-built schedule and call it the cause.
214
+ from trueppm_api.apps.projects.lifecycle import visible_projects
215
+
216
+ project_ids = list(
217
+ visible_projects(Project.objects.filter(program=program, is_deleted=False)).values_list(
218
+ "id", flat=True
219
+ )
220
+ )
221
+ if not project_ids:
222
+ return None
223
+
224
+ bands = _schedule_health_by_project(project_ids, today)
225
+ ranked = [(pid, band) for pid, band in bands.items() if band in _HEALTH_ORDINAL]
226
+ if not ranked:
227
+ return None
228
+
229
+ # Leaf rows only (#4276): a phase's `is_critical` is a rollup of its children
230
+ # (ADR-0024/ADR-0293, `_rollup_one_summary`), not a fact about the phase row
231
+ # itself, so counting the phase on top of the leaf whose value it rolled up
232
+ # double-counts that leaf and can tip the worst-project tie-break. See
233
+ # task_is_phase_expr's docstring.
234
+ critical_counts = {
235
+ r["project_id"]: r["c"]
236
+ for r in (
237
+ Task.objects.filter(project_id__in=project_ids, is_deleted=False, is_critical=True)
238
+ .exclude(status__in=_ACTIVE_EXCLUDE)
239
+ .annotate(_is_phase=task_is_phase_expr())
240
+ .filter(_is_phase=False)
241
+ .values("project_id")
242
+ .annotate(c=Count("id"))
243
+ )
244
+ }
245
+ names = dict(Project.objects.filter(id__in=project_ids).values_list("id", "name"))
246
+
247
+ # Sort by (health ordinal asc = worst first, critical count desc, name asc).
248
+ ranked.sort(
249
+ key=lambda pair: (
250
+ _HEALTH_ORDINAL[pair[1]],
251
+ -critical_counts.get(pair[0], 0),
252
+ names.get(pair[0], ""),
253
+ )
254
+ )
255
+ worst_id, worst_band = ranked[0]
256
+ return {"id": worst_id, "name": names.get(worst_id, ""), "health": worst_band}
257
+
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Per-project metric maps (grouped queries — no per-project loop)
261
+ # ---------------------------------------------------------------------------
262
+
263
+
264
+ def _committed_task_counts(project_ids: list[Any]) -> dict[Any, int]:
265
+ """project_id → committed (non-BACKLOG, non-deleted) task count, for weighting."""
266
+ if not project_ids:
267
+ return {}
268
+ rows = (
269
+ Task.objects.filter(project_id__in=project_ids, is_deleted=False)
270
+ .exclude(status=TaskStatus.BACKLOG)
271
+ .values("project_id")
272
+ .annotate(c=Count("id"))
273
+ )
274
+ return {r["project_id"]: r["c"] for r in rows}
275
+
276
+
277
+ def task_is_phase_expr() -> RawSQL:
278
+ """Boolean expression: this task row is a phase (has a structural child).
279
+
280
+ A phase's status, percent, and dates are a rollup of its children and are
281
+ never set directly (ADR-0293), so the *stored* ``status`` on a phase row is
282
+ not a fact about the work — a seeded or imported phase sits at
283
+ ``NOT_STARTED`` while every child is done. Any count that reads ``status``
284
+ must therefore count leaf work only, or a phase whose children are all
285
+ complete reads as late (#4238). Same ltree shape as the ``is_phase``
286
+ annotation on the task list; subtask children do not make a phase.
287
+ """
288
+ # nosemgrep: avoid-raw-sql — static SQL literal, no params interpolated.
289
+ return RawSQL(
290
+ "EXISTS("
291
+ " SELECT 1 FROM projects_task c"
292
+ " WHERE c.project_id = projects_task.project_id"
293
+ " AND c.is_deleted = false"
294
+ " AND c.is_subtask = false"
295
+ " AND c.id != projects_task.id"
296
+ " AND c.wbs_path IS NOT NULL"
297
+ " AND projects_task.wbs_path IS NOT NULL"
298
+ " AND c.wbs_path ~ (projects_task.wbs_path::text || '.*{1}')::lquery"
299
+ ")",
300
+ [],
301
+ output_field=BooleanField(),
302
+ )
303
+
304
+
305
+ def drift_counts_by_project(
306
+ project_ids: Collection[Any], today: datetime.date
307
+ ) -> dict[Any, tuple[int, int]]:
308
+ """project_id → ``(overdue_count, at_risk_count)`` for the program's project roster.
309
+
310
+ Same semantics as ``ProgramViewSet.projects`` (#560): incomplete tasks only
311
+ (``status`` != COMPLETE), leaf rows only. A phase's ``total_float`` and
312
+ ``early_finish`` are rollups of its children
313
+ (``scheduling.services._rollup_one_summary``) — not facts about the phase row
314
+ itself (ADR-0024/ADR-0293) — so counting the phase on top of the leaf whose
315
+ value it rolled up double-counts that leaf (#4276). See :func:`task_is_phase_expr`.
316
+
317
+ Built as a grouped query over ``Task`` rather than a reverse-FK
318
+ ``Count("tasks", filter=...)`` annotation — or a per-row correlated
319
+ ``Subquery`` — on a ``Project`` queryset, for the same reason as
320
+ :func:`risk_counts_by_project`: :func:`task_is_phase_expr`'s ``RawSQL`` names
321
+ its own FROM table (``projects_task``) literally, and that bare name is not
322
+ guaranteed once more than one reference to ``Task`` shares the same compiled
323
+ statement. A *joined* reverse-FK Count aliases the join; a *second*
324
+ correlated ``Subquery`` over ``Task`` in the same ``.annotate()`` call hits
325
+ the identical problem the other way (Django assigns ``U0``/``U1`` to
326
+ disambiguate the two subqueries' own FROM clauses) — confirmed by a failing
327
+ ``invalid reference to FROM-clause entry for table "projects_task"`` when
328
+ ``overdue_count`` and ``at_risk_count`` were each tried as their own
329
+ Subquery here. Keeping ``Task`` as the base model of one standalone query,
330
+ same shape as :func:`spi_counts_by_project`/:func:`risk_counts_by_project`,
331
+ side-steps both failure modes.
332
+
333
+ Projects with no qualifying leaf work are absent from the dict — callers
334
+ read with ``.get(pid, (0, 0))``.
335
+ """
336
+ if not project_ids:
337
+ return {}
338
+ incomplete = ~Q(status=TaskStatus.COMPLETE)
339
+ rows = (
340
+ Task.objects.filter(project_id__in=list(project_ids), is_deleted=False)
341
+ .filter(incomplete)
342
+ # Narrow before the correlated is_phase EXISTS runs (same reasoning as
343
+ # risk_counts_by_project's round-2 fix, #4250): a row that cannot
344
+ # contribute to either Count below no matter what _is_phase turns out
345
+ # to be is excluded first, so the per-row ltree EXISTS only runs for
346
+ # rows whose answer can matter.
347
+ .filter(Q(early_finish__lt=today) | (Q(total_float__isnull=False) & Q(total_float__lte=5)))
348
+ .annotate(_is_phase=task_is_phase_expr())
349
+ .filter(_is_phase=False)
350
+ .values("project_id")
351
+ .annotate(
352
+ overdue=Count("id", filter=Q(early_finish__lt=today)),
353
+ at_risk=Count("id", filter=Q(total_float__isnull=False) & Q(total_float__lte=5)),
354
+ )
355
+ )
356
+ return {r["project_id"]: (r["overdue"], r["at_risk"]) for r in rows}
357
+
358
+
359
+ def spi_counts_by_project(
360
+ project_ids: Collection[Any], today: datetime.date
361
+ ) -> dict[Any, tuple[int, int]]:
362
+ """project_id → ``(planned, planned_complete)`` for the SPI proxy.
363
+
364
+ The single source for both the project overview card
365
+ (``_project_spi_and_health``) and the program rollup, so the two can never
366
+ disagree about the same project.
367
+
368
+ **Baseline path — EVM semantics (#398), ``has_cpm_dates=True`` only (#4242).**
369
+ ``planned`` is the active baseline's rows with ``finish <= today``;
370
+ ``planned_complete`` is *every* task complete by today. The numerator is
371
+ deliberately wider than the denominator: work finished ahead of its baselined
372
+ date is earned value, so SPI can exceed 1.0. This path requires the active
373
+ baseline to carry real dates: a baseline captured with ``has_cpm_dates=False``
374
+ (snapshot taken before the CPM engine first ran, see :class:`Baseline`) has
375
+ mostly-or-entirely null ``BaselineTask.finish`` rows, which collapses
376
+ ``planned`` toward zero while ``planned_complete`` still counts every real
377
+ completion project-wide — an on-track-looking ratio over a near-empty
378
+ denominator next to a project that also reports late tasks (the hosted demo's
379
+ "GTM Readiness"/"Platform Core" symptom). A ``has_cpm_dates=False`` baseline is
380
+ therefore excluded from this path entirely (see the ``active_baseline`` query
381
+ below) and such a project falls through to the no-baseline path instead, per
382
+ the maintainer decision recorded on #4242: there is no fixed plan to be ahead
383
+ of until a baseline has real CPM dates.
384
+
385
+ **No-baseline path — one task set (#4238).** Both counts come from the *same*
386
+ rows: live leaf tasks (phase rows excluded, see :func:`task_is_phase_expr`)
387
+ whose CPM ``early_finish <= today``, and of those, the ones complete by today.
388
+ Without a (usable) baseline there is no fixed plan to be ahead of — the
389
+ reference is the live CPM forecast, and a COMPLETE task is pinned to its
390
+ actuals on the next run. A complete task *outside* that window therefore has a
391
+ null or stale ``early_finish``, not an ahead-of-plan finish, and counting it in
392
+ the numerator let it cancel out genuinely late rows: the overview read
393
+ ``on_track`` / ``spi=1.0`` in a project that also reported late tasks. Scoped
394
+ this way the ratio cannot exceed 1.0 and drops for exactly the rows
395
+ ``tasks_late_count`` counts (the same ``early_finish`` reference). A project
396
+ whose active baseline has ``has_cpm_dates=False`` is routed here too (#4242),
397
+ so the same cap applies to it.
398
+
399
+ A null ``actual_finish`` on a COMPLETE task still counts as complete by today —
400
+ keying on ``actual_finish`` stops a late completion masquerading as on-time,
401
+ but a missing stamp is not evidence of lateness.
402
+
403
+ Projects with no planned-by-today work are absent from the result.
404
+ """
405
+ if not project_ids:
406
+ return {}
407
+ complete_by_today = Q(status=TaskStatus.COMPLETE) & (
408
+ Q(actual_finish__lte=today) | Q(actual_finish__isnull=True)
409
+ )
410
+
411
+ # Active baseline per project (at most one — is_active is a per-project flag).
412
+ # has_cpm_dates=True only (#4242): a baseline snapshotted before CPM first ran
413
+ # has mostly-or-entirely null BaselineTask.finish, so it is not a usable EVM
414
+ # plan — see the docstring above. A has_cpm_dates=False active baseline is
415
+ # therefore treated as if the project had no active baseline at all, which
416
+ # routes it into the no_baseline path below.
417
+ active_baseline = dict(
418
+ Baseline.objects.filter(
419
+ project_id__in=list(project_ids),
420
+ is_active=True,
421
+ is_deleted=False,
422
+ has_cpm_dates=True,
423
+ ).values_list("project_id", "id")
424
+ )
425
+
426
+ out: dict[Any, tuple[int, int]] = {}
427
+
428
+ if active_baseline:
429
+ baseline_to_project = {bid: pid for pid, bid in active_baseline.items()}
430
+ planned_by_project: dict[Any, int] = {}
431
+ for br in (
432
+ BaselineTask.objects.filter(
433
+ baseline_id__in=list(active_baseline.values()), finish__lte=today
434
+ )
435
+ .values("baseline_id")
436
+ .annotate(c=Count("id"))
437
+ ):
438
+ planned_by_project[baseline_to_project[br["baseline_id"]]] = br["c"]
439
+ if planned_by_project:
440
+ completed_by_project = {
441
+ cr["project_id"]: cr["c"]
442
+ for cr in (
443
+ Task.objects.filter(project_id__in=list(planned_by_project), is_deleted=False)
444
+ .filter(complete_by_today)
445
+ .values("project_id")
446
+ .annotate(c=Count("id"))
447
+ )
448
+ }
449
+ for pid, planned in planned_by_project.items():
450
+ out[pid] = (planned, completed_by_project.get(pid, 0))
451
+
452
+ no_baseline = [pid for pid in project_ids if pid not in active_baseline]
453
+ if no_baseline:
454
+ for fr in (
455
+ Task.objects.filter(
456
+ project_id__in=no_baseline, is_deleted=False, early_finish__lte=today
457
+ )
458
+ .annotate(_is_phase=task_is_phase_expr())
459
+ .filter(_is_phase=False)
460
+ .values("project_id")
461
+ .annotate(planned=Count("id"), complete=Count("id", filter=complete_by_today))
462
+ ):
463
+ out[fr["project_id"]] = (fr["planned"], fr["complete"])
464
+
465
+ return out
466
+
467
+
468
+ def risk_counts_by_project(project_ids: Collection[Any]) -> dict[Any, tuple[int, int]]:
469
+ """project_id → ``(at_risk_count, critical_count)`` for the status/health triage counts.
470
+
471
+ Same semantics as ``ProjectViewSet.status_summary``: incomplete tasks only
472
+ (``status`` != COMPLETE), leaf rows only. A phase's ``total_float`` and
473
+ ``is_critical`` are rollups of its children
474
+ (``scheduling.services._rollup_one_summary``: ``total_float = min(child floats)``,
475
+ ``is_critical = any(child.is_critical)``) — not facts about the phase row itself
476
+ (ADR-0024/ADR-0293) — so counting the phase on top of the leaf whose value it
477
+ rolled up double-counts that leaf (#4250). See :func:`task_is_phase_expr`.
478
+
479
+ Built as a grouped query over ``Task`` rather than a reverse-FK
480
+ ``Count("tasks", filter=...)`` annotation on a ``Project`` queryset, because
481
+ :func:`task_is_phase_expr`'s ``RawSQL`` names its own FROM table
482
+ (``projects_task``) literally. On a ``Project`` queryset the join Django adds
483
+ for ``tasks`` is not guaranteed to resolve to that bare table name — it can be
484
+ aliased, particularly once a second filtered aggregate over the same relation
485
+ is added — so the exclusion could silently match against the wrong joined row
486
+ instead of raising. Keeping ``Task`` as the base model side-steps that
487
+ entirely; same shape as :func:`spi_counts_by_project`.
488
+
489
+ Projects with no incomplete leaf work are absent from the dict — callers read
490
+ with ``.get(pid, (0, 0))``.
491
+ """
492
+ if not project_ids:
493
+ return {}
494
+ incomplete = ~Q(status=TaskStatus.COMPLETE)
495
+ rows = (
496
+ Task.objects.filter(project_id__in=list(project_ids), is_deleted=False)
497
+ # Narrow before the correlated is_phase EXISTS runs: a row that is
498
+ # complete, or incomplete but neither critical nor low-float, cannot
499
+ # contribute to either Count below no matter what _is_phase turns out
500
+ # to be, so excluding it here is semantics-preserving (NULL total_float
501
+ # and NULL is_critical already fail both conditions in SQL) and avoids
502
+ # evaluating the per-row ltree EXISTS for rows whose answer can't matter.
503
+ .filter(incomplete)
504
+ .filter(Q(is_critical=True) | Q(total_float__lte=5))
505
+ .annotate(_is_phase=task_is_phase_expr())
506
+ .filter(_is_phase=False)
507
+ .values("project_id")
508
+ .annotate(
509
+ at_risk=Count(
510
+ "id",
511
+ filter=Q(total_float__isnull=False) & Q(total_float__lte=5),
512
+ ),
513
+ critical=Count("id", filter=Q(is_critical=True)),
514
+ )
515
+ )
516
+ return {r["project_id"]: (r["at_risk"], r["critical"]) for r in rows}
517
+
518
+
519
+ def spi_health_band(spi: float) -> str:
520
+ """Map an SPI proxy onto the three health bands (shared with the overview card)."""
521
+ if spi >= 0.95:
522
+ return "on_track"
523
+ if spi >= 0.85:
524
+ return "at_risk"
525
+ return "critical"
526
+
527
+
528
+ def _schedule_health_by_project(project_ids: list[Any], today: datetime.date) -> dict[Any, str]:
529
+ """project_id → SPI-proxy health band, matching ProjectOverviewView semantics.
530
+
531
+ Counts come from :func:`spi_counts_by_project`, the same function the project
532
+ overview uses. A project with no due-by-today work is ``unknown`` and is
533
+ excluded from the program reduce.
534
+ """
535
+ counts = spi_counts_by_project(project_ids, today)
536
+ out: dict[Any, str] = {}
537
+ for pid in project_ids:
538
+ planned, complete = counts.get(pid, (0, 0))
539
+ out[pid] = "unknown" if planned <= 0 else spi_health_band(complete / planned)
540
+ return out
541
+
542
+
543
+ def _milestone_band(milestones: list[Any], today: datetime.date, soon: datetime.date) -> str:
544
+ """Worst health band across one project's milestones.
545
+
546
+ ``critical`` dominates ``at_risk``, so an overdue milestone short-circuits the
547
+ scan — no later milestone can improve the band. A milestone that is complete
548
+ or has no finish date contributes nothing either way.
549
+ """
550
+ if not milestones:
551
+ return "unknown"
552
+ band = "on_track"
553
+ for m in milestones:
554
+ done = m["status"] == TaskStatus.COMPLETE or (m["percent_complete"] or 0) >= 100
555
+ ef = m["early_finish"]
556
+ if done or ef is None:
557
+ continue
558
+ if ef < today:
559
+ return "critical"
560
+ if ef <= soon:
561
+ band = "at_risk"
562
+ return band
563
+
564
+
565
+ def _milestone_health_by_project(project_ids: list[Any], today: datetime.date) -> dict[Any, str]:
566
+ """project_id → milestone health band from milestone task dates.
567
+
568
+ critical if any milestone is overdue and not complete; at_risk if any
569
+ milestone is due within 7 days and not yet complete; on_track if the project
570
+ has milestones and none are late/at-risk; unknown if it has no milestones
571
+ (excluded from the reduce).
572
+ """
573
+ if not project_ids:
574
+ return {}
575
+ grouped: dict[Any, list[Any]] = {pid: [] for pid in project_ids}
576
+ for r in Task.objects.filter(
577
+ project_id__in=project_ids, is_deleted=False, is_milestone=True
578
+ ).values("project_id", "early_finish", "status", "percent_complete"):
579
+ grouped[r["project_id"]].append(r)
580
+
581
+ soon = today + datetime.timedelta(days=7)
582
+ return {pid: _milestone_band(ms, today, soon) for pid, ms in grouped.items()}
583
+
584
+
585
+ def _baseline_variance_by_project(project_ids: list[Any]) -> dict[Any, float]:
586
+ """project_id → projected-end drift in calendar days vs the active baseline.
587
+
588
+ The current projected end (the latest ``Task.early_finish``) minus the baseline
589
+ end (the latest ``BaselineTask.finish``). Positive = the project is trending
590
+ later than baseline. Projects without an active baseline are absent from the
591
+ map (no comparison basis).
592
+
593
+ Measured in working time (#4197): each end is read with the edge of its day it
594
+ sits on (:func:`~trueppm_api.apps.scheduling.finish_reading.latest_finish`), so
595
+ a plan whose finishing milestone moves from the end of a Friday to the start of
596
+ the next Monday reports 0, not +3. Only the rows *on* each end day are read for
597
+ that — the max itself stays a grouped aggregate.
598
+
599
+ ``has_cpm_dates=True`` only (#4242): a baseline snapshotted before CPM first
600
+ ran has mostly-or-entirely null ``BaselineTask.finish``, so ``Max("finish")``
601
+ is either ``None`` (already excluded below) or a date computed from an
602
+ incomplete subset of rows — not the baseline's real projected end. Same
603
+ exclusion as :func:`spi_counts_by_project`: such a project is treated as
604
+ having no active baseline, and is absent from the returned map.
605
+ """
606
+ from trueppm_api.apps.scheduling.calendars import project_sched_calendars
607
+ from trueppm_api.apps.scheduling.finish_reading import (
608
+ FINISH_READING_FIELDS,
609
+ finish_shift_days,
610
+ latest_finish,
611
+ task_finish_at_day_start,
612
+ )
613
+
614
+ if not project_ids:
615
+ return {}
616
+ active_baseline = dict(
617
+ Baseline.objects.filter(
618
+ project_id__in=project_ids,
619
+ is_active=True,
620
+ is_deleted=False,
621
+ has_cpm_dates=True,
622
+ ).values_list("project_id", "id")
623
+ )
624
+ if not active_baseline:
625
+ return {}
626
+
627
+ current_end = {
628
+ r["project_id"]: r["end"]
629
+ for r in Task.objects.filter(project_id__in=list(active_baseline.keys()), is_deleted=False)
630
+ .values("project_id")
631
+ .annotate(end=Max("early_finish"))
632
+ if r["end"] is not None
633
+ }
634
+ baseline_to_project = {bid: pid for pid, bid in active_baseline.items()}
635
+ baseline_end = {
636
+ baseline_to_project[r["baseline_id"]]: r["end"]
637
+ for r in BaselineTask.objects.filter(baseline_id__in=list(active_baseline.values()))
638
+ .values("baseline_id")
639
+ .annotate(end=Max("finish"))
640
+ if r["end"] is not None
641
+ }
642
+ compared = [pid for pid in active_baseline if pid in current_end and pid in baseline_end]
643
+ if not compared:
644
+ return {}
645
+
646
+ # The rows ON each end day, with their reading (#4197). ``__in`` over the end
647
+ # days can over-select another project's day; the equality check drops those.
648
+ current_rows: dict[Any, list[tuple[Any, bool, Any]]] = {}
649
+ for t in Task.objects.filter(
650
+ project_id__in=compared,
651
+ is_deleted=False,
652
+ early_finish__in={current_end[pid] for pid in compared},
653
+ ).only("project_id", "early_finish", "wbs_path", *FINISH_READING_FIELDS):
654
+ if t.early_finish == current_end[t.project_id]:
655
+ current_rows.setdefault(t.project_id, []).append(
656
+ (t.early_finish, task_finish_at_day_start(t), t.wbs_path)
657
+ )
658
+ base_rows_raw = [
659
+ r
660
+ for r in BaselineTask.objects.filter(
661
+ baseline_id__in=[active_baseline[pid] for pid in compared],
662
+ finish__in={baseline_end[pid] for pid in compared},
663
+ ).values("baseline_id", "task_id", "finish", "finish_at_day_start")
664
+ if r["finish"] == baseline_end[baseline_to_project[r["baseline_id"]]]
665
+ ]
666
+ # A baseline row carries no WBS path; the task's (live or soft-deleted) path is
667
+ # what lets latest_finish drop a summary row sharing its leaf's end day.
668
+ wbs_by_task = dict(
669
+ Task.objects.filter(id__in={r["task_id"] for r in base_rows_raw}).values_list(
670
+ "id", "wbs_path"
671
+ )
672
+ )
673
+ base_rows: dict[Any, list[tuple[Any, bool, Any]]] = {}
674
+ base_unknown: set[Any] = set()
675
+ for r in base_rows_raw:
676
+ pid = baseline_to_project[r["baseline_id"]]
677
+ if r["finish_at_day_start"] is None:
678
+ base_unknown.add(pid)
679
+ base_rows.setdefault(pid, []).append(
680
+ (r["finish"], bool(r["finish_at_day_start"]), wbs_by_task.get(r["task_id"]))
681
+ )
682
+
683
+ readings: dict[Any, tuple[Any, Any]] = {}
684
+ for pid in compared:
685
+ cur = latest_finish(current_rows.get(pid, [])) or (current_end[pid], False)
686
+ base = latest_finish(base_rows.get(pid, [])) or (baseline_end[pid], None)
687
+ if pid in base_unknown:
688
+ # A baseline captured before the reading was recorded (#4197): unknown.
689
+ base = (base[0], None)
690
+ readings[pid] = (base, cur)
691
+ calendars = project_sched_calendars(
692
+ pid for pid, (base, cur) in readings.items() if base[1] or cur[1]
693
+ )
694
+ return {
695
+ pid: float(finish_shift_days(base, cur, calendars.get(str(pid))))
696
+ for pid, (base, cur) in readings.items()
697
+ }
698
+
699
+
700
+ def _schedule_variance_by_project(project_ids: list[Any]) -> dict[Any, float]:
701
+ """project_id → mean lateness of completed work, in calendar days.
702
+
703
+ Per-task ``actual_finish − baseline_finish`` (the quantity serializers
704
+ ``get_schedule_variance_days`` reports per task) averaged over the project's
705
+ completed tasks that exist in the active baseline. Distinct from
706
+ ``baseline_variance``: SV measures *how late finished work landed*, not where
707
+ the project end is heading. Absent for projects without an active baseline or
708
+ with no matched completed work.
709
+
710
+ Each delta is measured in working time (#4197): an actual finish is a recorded
711
+ day, read as its end, and a baselined start-of-day milestone finish is read as
712
+ the end of the working day before it — so a milestone baselined at the start
713
+ of a Monday and hit on the Friday before landed on time, not three days early.
714
+
715
+ ``has_cpm_dates=True`` only (#4242): same exclusion as
716
+ :func:`spi_counts_by_project` and :func:`_baseline_variance_by_project` — a
717
+ baseline snapshotted before CPM first ran has mostly-or-entirely null
718
+ ``BaselineTask.finish`` and is not a usable plan to measure lateness against.
719
+ """
720
+ from trueppm_api.apps.scheduling.calendars import project_sched_calendars
721
+ from trueppm_api.apps.scheduling.finish_reading import finish_shift_days
722
+
723
+ if not project_ids:
724
+ return {}
725
+ active_baseline = dict(
726
+ Baseline.objects.filter(
727
+ project_id__in=project_ids,
728
+ is_active=True,
729
+ is_deleted=False,
730
+ has_cpm_dates=True,
731
+ ).values_list("project_id", "id")
732
+ )
733
+ if not active_baseline:
734
+ return {}
735
+
736
+ # Baseline finish, with its edge of the day, per (project, task_id).
737
+ baseline_to_project = {bid: pid for pid, bid in active_baseline.items()}
738
+ baseline_finish: dict[tuple[Any, Any], tuple[datetime.date, bool | None]] = {}
739
+ for bt in BaselineTask.objects.filter(
740
+ baseline_id__in=list(active_baseline.values()), finish__isnull=False
741
+ ).values("baseline_id", "task_id", "finish", "finish_at_day_start"):
742
+ if bt["finish"] is None: # narrows for mypy; excluded by the filter
743
+ continue
744
+ baseline_finish[(baseline_to_project[bt["baseline_id"]], bt["task_id"])] = (
745
+ bt["finish"],
746
+ bt["finish_at_day_start"],
747
+ )
748
+
749
+ # Completed tasks with an actual finish.
750
+ pairs: dict[Any, list[tuple[tuple[datetime.date, bool | None], datetime.date]]] = {}
751
+ for t in Task.objects.filter(
752
+ project_id__in=list(active_baseline.keys()),
753
+ is_deleted=False,
754
+ status=TaskStatus.COMPLETE,
755
+ actual_finish__isnull=False,
756
+ ).values("id", "project_id", "actual_finish"):
757
+ base = baseline_finish.get((t["project_id"], t["id"]))
758
+ actual = t["actual_finish"]
759
+ if base is not None and actual is not None:
760
+ pairs.setdefault(t["project_id"], []).append((base, actual))
761
+
762
+ # A calendar only for the projects with a start-of-day baseline finish; an
763
+ # end-of-day pair diffs identically without one.
764
+ calendars = project_sched_calendars(
765
+ pid for pid, ps in pairs.items() if any(base[1] for base, _ in ps)
766
+ )
767
+ deltas = {
768
+ pid: [
769
+ finish_shift_days(base, (actual, False), calendars.get(str(pid))) for base, actual in ps
770
+ ]
771
+ for pid, ps in pairs.items()
772
+ }
773
+ return {pid: sum(ds) / len(ds) for pid, ds in deltas.items() if ds}
774
+
775
+
776
+ # ---------------------------------------------------------------------------
777
+ # Program-total KPIs (policy-independent)
778
+ # ---------------------------------------------------------------------------
779
+
780
+
781
+ def _critical_task_total(project_ids: list[Any]) -> int:
782
+ """Total open critical-path tasks across the program.
783
+
784
+ Leaf rows only (#4276): a phase's `is_critical` is a rollup of its children
785
+ (ADR-0024/ADR-0293, `_rollup_one_summary`: `is_critical = any(child.is_critical)`),
786
+ not a fact about the phase row itself, so counting the phase on top of the
787
+ leaf whose value it rolled up double-counts that leaf and inflates this
788
+ program-total KPI. See :func:`task_is_phase_expr`.
789
+ """
790
+ if not project_ids:
791
+ return 0
792
+ return (
793
+ Task.objects.filter(project_id__in=project_ids, is_deleted=False, is_critical=True)
794
+ .exclude(status__in=_ACTIVE_EXCLUDE)
795
+ .annotate(_is_phase=task_is_phase_expr())
796
+ .filter(_is_phase=False)
797
+ .count()
798
+ )
799
+
800
+
801
+ def _at_risk_task_total(project_ids: list[Any]) -> int:
802
+ """Total open tasks with ≤ 5 working days of float across the program.
803
+
804
+ Leaf rows only (#4276), same rollup reasoning as :func:`_critical_task_total`:
805
+ a phase's `total_float` is `min(child floats)` (ADR-0024/ADR-0293), not a fact
806
+ about the phase row itself.
807
+ """
808
+ if not project_ids:
809
+ return 0
810
+ return (
811
+ Task.objects.filter(
812
+ project_id__in=project_ids,
813
+ is_deleted=False,
814
+ total_float__isnull=False,
815
+ total_float__lte=5,
816
+ )
817
+ .exclude(status__in=_ACTIVE_EXCLUDE)
818
+ .annotate(_is_phase=task_is_phase_expr())
819
+ .filter(_is_phase=False)
820
+ .count()
821
+ )
822
+
823
+
824
+ def _risk_score_total(project_ids: list[Any]) -> int:
825
+ """Total open-risk exposure (Σ probability × impact) across the program."""
826
+ if not project_ids:
827
+ return 0
828
+ agg = Risk.objects.filter(
829
+ project_id__in=project_ids,
830
+ status__in=[RiskStatus.OPEN, RiskStatus.MITIGATING],
831
+ ).aggregate(score=Sum(F("probability") * F("impact")))
832
+ return int(agg["score"] or 0)
833
+
834
+
835
+ # ---------------------------------------------------------------------------
836
+ # Reducers
837
+ # ---------------------------------------------------------------------------
838
+
839
+
840
+ def _reduce_health(
841
+ bands: list[str], policy: str, task_weights: dict[Any, int], project_ids: list[Any]
842
+ ) -> str:
843
+ """Combine per-project health bands into one band under the policy.
844
+
845
+ ``unknown`` bands are dropped before reducing (a project with no data must
846
+ not make the program look worse). Returns ``unknown`` when every project is
847
+ unknown or there are no projects.
848
+
849
+ Note: task-weighting health requires aligning weights to bands positionally,
850
+ so the caller passes bands in ``project_ids`` order for ``task_weighted``.
851
+ """
852
+ ordinals = [_HEALTH_ORDINAL[b] for b in bands if b in _HEALTH_ORDINAL]
853
+ if not ordinals:
854
+ return "unknown"
855
+
856
+ if policy == AggregationPolicy.WORST.value:
857
+ return _ORDINAL_TO_HEALTH[min(ordinals)]
858
+ if policy == AggregationPolicy.TASK_WEIGHTED.value:
859
+ # Weight each project's band by its committed task count. bands is in
860
+ # project_ids order; pair them and skip unknowns.
861
+ weighted_sum = 0.0
862
+ weight_total = 0.0
863
+ for pid, band in zip(project_ids, bands, strict=False):
864
+ if band not in _HEALTH_ORDINAL:
865
+ continue
866
+ w = task_weights.get(pid, 0) or 1 # a zero-task project still counts once
867
+ weighted_sum += _HEALTH_ORDINAL[band] * w
868
+ weight_total += w
869
+ if weight_total == 0:
870
+ return _ORDINAL_TO_HEALTH[round(sum(ordinals) / len(ordinals))]
871
+ return _ORDINAL_TO_HEALTH[round(weighted_sum / weight_total)]
872
+ # AVERAGE (and the weighted_by_budget fallback).
873
+ return _ORDINAL_TO_HEALTH[round(sum(ordinals) / len(ordinals))]
874
+
875
+
876
+ def _reduce_variance(
877
+ by_project: dict[Any, float], policy: str, task_weights: dict[Any, int]
878
+ ) -> float | None:
879
+ """Combine per-project day-variances under the policy.
880
+
881
+ ``worst`` = the largest slip (the project dragging the program). ``average``
882
+ = arithmetic mean. ``task_weighted`` = mean weighted by committed task count.
883
+ Returns ``None`` when no project has a value (e.g. no active baselines), which
884
+ the serializer renders as "—".
885
+ """
886
+ if not by_project:
887
+ return None
888
+ values = list(by_project.values())
889
+
890
+ if policy == AggregationPolicy.WORST.value:
891
+ return round(max(values), 1)
892
+ if policy == AggregationPolicy.TASK_WEIGHTED.value:
893
+ weighted_sum = 0.0
894
+ weight_total = 0.0
895
+ for pid, val in by_project.items():
896
+ w = task_weights.get(pid, 0) or 1
897
+ weighted_sum += val * w
898
+ weight_total += w
899
+ if weight_total == 0:
900
+ return round(sum(values) / len(values), 1)
901
+ return round(weighted_sum / weight_total, 1)
902
+ # AVERAGE (and the weighted_by_budget fallback).
903
+ return round(sum(values) / len(values), 1)