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,551 @@
1
+ """Export TruePPM project data to MS Project XML format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import xml.etree.ElementTree as ET
6
+ from datetime import date
7
+ from typing import Any
8
+
9
+ from trueppm_api.apps.msproject.extended_attributes import (
10
+ DURATION1_FIELD_ID,
11
+ DURATION2_FIELD_ID,
12
+ DURATION3_FIELD_ID,
13
+ DURATION4_FIELD_ID,
14
+ PERT_ALIAS_LABELS,
15
+ PERT_EXPECTED_FORMULA,
16
+ PERT_FIELD_NAMES,
17
+ )
18
+ from trueppm_api.apps.scheduling.units import (
19
+ HOURS_PER_WORKING_DAY,
20
+ MSPDI_LAG_TENTHS_PER_WORKING_DAY,
21
+ )
22
+
23
+ # MS Project XML namespace.
24
+ _NS = "http://schemas.microsoft.com/project"
25
+
26
+ # MSPDI <ConstraintType> codes this exporter emits (#2891). Named rather than
27
+ # inlined because the importer's mapping table (`CONSTRAINT_TYPES_APPLIED_AS_SNET`
28
+ # in `dataclasses`) has to stay in agreement with them for the round trip to close.
29
+ MSPDI_CONSTRAINT_AS_SOON_AS_POSSIBLE = 0
30
+ MSPDI_CONSTRAINT_START_NO_EARLIER_THAN = 4
31
+
32
+ # TruePPM dep_type -> MS Project PredecessorLink/Type.
33
+ _DEP_TYPE_TO_LINK_TYPE = {
34
+ "FF": "0",
35
+ "FS": "1",
36
+ "SF": "2",
37
+ "SS": "3",
38
+ }
39
+
40
+ # MSPDI WeekDay/DayType (1=Sunday … 7=Saturday) paired with the TruePPM
41
+ # Calendar.working_days bit (Mon=1 … Sun=64), in MSPDI emission order.
42
+ # Keep in lockstep with parser._DAY_TYPE_TO_BIT.
43
+ _DAY_TYPES_AND_BITS = (
44
+ ("1", 64), # Sunday
45
+ ("2", 1), # Monday
46
+ ("3", 2), # Tuesday
47
+ ("4", 4), # Wednesday
48
+ ("5", 8), # Thursday
49
+ ("6", 16), # Friday
50
+ ("7", 32), # Saturday
51
+ )
52
+
53
+
54
+ def export_project_xml(project_id: str) -> bytes:
55
+ """Export a TruePPM project to MS Project XML format.
56
+
57
+ Returns:
58
+ UTF-8 encoded XML bytes.
59
+ """
60
+ from trueppm_api.apps.projects.models import Dependency, Project, Task
61
+ from trueppm_api.apps.resources.models import Resource, TaskResource
62
+ from trueppm_api.apps.scheduling.calendars import resolve_applied_calendars
63
+
64
+ # calendar__exceptions + calendar_layers__calendar__exceptions: the calendar
65
+ # emission below walks every applied calendar's exceptions (#1769).
66
+ project = Project.objects.prefetch_related(
67
+ "calendar__exceptions", "calendar_layers__calendar__exceptions"
68
+ ).get(pk=project_id)
69
+ applied_calendars = resolve_applied_calendars(project)
70
+ tasks = list(
71
+ Task.objects.filter(project_id=project_id, is_deleted=False).order_by("wbs_path", "name")
72
+ )
73
+ dependencies = list(
74
+ Dependency.objects.filter(
75
+ predecessor__project_id=project_id, is_deleted=False
76
+ ).select_related("predecessor", "successor")
77
+ )
78
+ task_ids = [str(t.pk) for t in tasks]
79
+ assignments = list(TaskResource.objects.filter(task_id__in=task_ids).select_related("resource"))
80
+ resource_ids = {a.resource_id for a in assignments}
81
+ resources = list(Resource.objects.filter(pk__in=resource_ids))
82
+
83
+ # UID maps - MS Project expects sequential integer UIDs.
84
+ task_pk_to_uid = {str(t.pk): i for i, t in enumerate(tasks, start=1)}
85
+ resource_pk_to_uid = {str(r.pk): i for i, r in enumerate(resources, start=1)}
86
+ dep_by_successor = _build_dep_lookup(dependencies)
87
+
88
+ # --- Build XML ---
89
+ ET.register_namespace("", _NS)
90
+ root = ET.Element(f"{{{_NS}}}Project")
91
+
92
+ _sub_text(root, "Name", project.name)
93
+ _sub_text(root, "StartDate", _format_date(project.start_date))
94
+ if applied_calendars:
95
+ # The merged working calendar is always emitted as UID 1 (see
96
+ # _add_calendars_block). Header element order follows the MSPDI schema:
97
+ # CalendarUID sits among the project properties, before
98
+ # ExtendedAttributes and Calendars.
99
+ _sub_text(root, "CalendarUID", "1")
100
+
101
+ # --- Project-level ExtendedAttribute definitions (#798, ADR-0093) ---
102
+ # Emit the four PERT slots only when at least one non-summary, non-milestone
103
+ # task carries all three values. MS Project tolerates the block being
104
+ # absent; emitting it unconditionally would clutter files that don't use
105
+ # three-point estimates.
106
+ pert_tasks = _select_pert_tasks(tasks)
107
+ if pert_tasks:
108
+ _add_pert_extended_attribute_defs(root)
109
+
110
+ # --- Calendars (#1769) ---
111
+ # Emitted between ExtendedAttributes and Tasks, matching the MSPDI element
112
+ # order. Omitted entirely when the project has no applied calendar (the
113
+ # engine's implicit Mon-Fri/8h default), which MS Project also defaults to.
114
+ if applied_calendars:
115
+ _add_calendars_block(root, applied_calendars)
116
+
117
+ # --- Tasks ---
118
+ tasks_el = ET.SubElement(root, f"{{{_NS}}}Tasks")
119
+
120
+ # Task UID 0: project summary task
121
+ _add_summary_task(tasks_el, project)
122
+
123
+ pert_task_pks = {str(t.pk) for t in pert_tasks}
124
+ day_end_time = _milestone_day_end_time(applied_calendars)
125
+ for task in tasks:
126
+ _add_task_element(
127
+ tasks_el,
128
+ task,
129
+ task_pk_to_uid[str(task.pk)],
130
+ is_pert=str(task.pk) in pert_task_pks,
131
+ preds=dep_by_successor.get(str(task.pk), []),
132
+ task_pk_to_uid=task_pk_to_uid,
133
+ day_end_time=day_end_time,
134
+ )
135
+
136
+ _add_resources_block(root, resources, resource_pk_to_uid)
137
+ _add_assignments_block(root, assignments, task_pk_to_uid, resource_pk_to_uid)
138
+
139
+ # Serialize
140
+ tree = ET.ElementTree(root)
141
+ ET.indent(tree, space=" ")
142
+
143
+ xml_decl = b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
144
+ return xml_decl + ET.tostring(root, encoding="unicode").encode("utf-8")
145
+
146
+
147
+ def _build_dep_lookup(dependencies: list[Any]) -> dict[str, list[tuple[str, str, int]]]:
148
+ """Index dependencies by successor: ``successor_pk -> [(pred_pk, dep_type, lag)]``."""
149
+ dep_by_successor: dict[str, list[tuple[str, str, int]]] = {}
150
+ for dep in dependencies:
151
+ dep_by_successor.setdefault(str(dep.successor_id), []).append(
152
+ (str(dep.predecessor_id), dep.dep_type, dep.lag)
153
+ )
154
+ return dep_by_successor
155
+
156
+
157
+ def _select_pert_tasks(tasks: list[Any]) -> list[Any]:
158
+ """Return the leaf, non-milestone tasks carrying all three PERT durations.
159
+
160
+ The summary set is precomputed once for O(N) filtering — a summary or
161
+ milestone task never emits three-point values (ADR-0093 Q5).
162
+ """
163
+ summary_pks = _summary_pks_from_tasks(tasks)
164
+ return [
165
+ t
166
+ for t in tasks
167
+ if (
168
+ str(t.pk) not in summary_pks
169
+ and not t.is_milestone
170
+ and t.optimistic_duration is not None
171
+ and t.most_likely_duration is not None
172
+ and t.pessimistic_duration is not None
173
+ )
174
+ ]
175
+
176
+
177
+ def _add_task_dates(task_el: ET.Element, task: Any, day_end_time: str = "00:00:00") -> None:
178
+ """Emit the contiguous Start/Finish/Constraint/Actual date run for one ``<Task>``.
179
+
180
+ MSPDI is order-sensitive, and ``ET.SubElement`` appends at call time, so the
181
+ caller's invocation position *is* the positional guarantee for every element
182
+ below — it must stay exactly where it sits in ``_add_task_element``'s append
183
+ sequence (after ``Notes``, before the PERT ExtendedAttribute values).
184
+
185
+ Constraint + actuals round-trip (#2891). ``planned_start`` IS a
186
+ start-no-earlier-than floor, so emitting it as MSPDI ConstraintType 4 with a
187
+ ConstraintDate is a faithful translation rather than an approximation — and
188
+ it is what makes the round trip closed: the importer reads the constraint
189
+ date back as ``planned_start``. Emitting only <Start> (which is CPM output, and
190
+ per ADR-0132 a remaining-work date on a partially-complete task) meant a
191
+ re-import silently promoted computed dates to committed ones.
192
+
193
+ Tasks with no floor get ConstraintType 0 (As Soon As Possible), which is
194
+ TruePPM's own default: an omitted element would leave MS Project to infer a
195
+ constraint from the <Start> we emit, re-creating the same promotion.
196
+
197
+ End-of-day milestones (#4079). A milestone that follows work sits at the END
198
+ of its ``early_start`` day, and MS Project places it there too — at the
199
+ calendar's finish time, not at midnight. Writing midnight would put it at the
200
+ start of the day, before its own predecessor finishes. ``day_end_time`` is
201
+ the end of the working day MS Project will schedule the file on
202
+ (:func:`_milestone_day_end_time`).
203
+ """
204
+ at_day_end = bool(task.is_milestone and getattr(task, "milestone_at_day_end", False))
205
+ time_of_day = day_end_time if at_day_end else "00:00:00"
206
+ if task.early_start:
207
+ _sub_text(task_el, "Start", _format_date(task.early_start, time_of_day))
208
+ elif task.planned_start:
209
+ _sub_text(task_el, "Start", _format_date(task.planned_start))
210
+ if task.early_finish:
211
+ _sub_text(task_el, "Finish", _format_date(task.early_finish, time_of_day))
212
+
213
+ if task.planned_start:
214
+ _sub_text(task_el, "ConstraintType", str(MSPDI_CONSTRAINT_START_NO_EARLIER_THAN))
215
+ _sub_text(task_el, "ConstraintDate", _format_date(task.planned_start))
216
+ else:
217
+ _sub_text(task_el, "ConstraintType", str(MSPDI_CONSTRAINT_AS_SOON_AS_POSSIBLE))
218
+ # A milestone closed after work keeps its end-of-day edge (#4433), and MS
219
+ # Project reads a 100%-complete task's Finish from its ActualFinish: the
220
+ # actuals carry the same time of day as Start/Finish, or the re-imported
221
+ # milestone sits at the start of its day and its successors land a day early.
222
+ if task.actual_start:
223
+ _sub_text(task_el, "ActualStart", _format_date(task.actual_start, time_of_day))
224
+ if task.actual_finish:
225
+ _sub_text(task_el, "ActualFinish", _format_date(task.actual_finish, time_of_day))
226
+
227
+
228
+ def _add_task_element(
229
+ tasks_el: ET.Element,
230
+ task: Any,
231
+ uid: int,
232
+ *,
233
+ is_pert: bool,
234
+ preds: list[tuple[str, str, int]],
235
+ task_pk_to_uid: dict[str, int],
236
+ day_end_time: str = "00:00:00",
237
+ ) -> None:
238
+ """Emit one ``<Task>`` element, including PERT values and predecessor links."""
239
+ task_el = ET.SubElement(tasks_el, f"{{{_NS}}}Task")
240
+ _sub_text(task_el, "UID", str(uid))
241
+ _sub_text(task_el, "Name", task.name)
242
+ _sub_text(task_el, "Duration", _days_to_duration(task.duration))
243
+ _sub_text(task_el, "DurationFormat", "7")
244
+ if task.wbs_path:
245
+ _sub_text(task_el, "OutlineNumber", task.wbs_path)
246
+ _sub_text(task_el, "OutlineLevel", str(task.wbs_path.count(".") + 1))
247
+ else:
248
+ _sub_text(task_el, "OutlineLevel", "1")
249
+ _sub_text(task_el, "Milestone", "1" if task.is_milestone else "0")
250
+ # Task.percent_complete is already a 0-100 percent (#1759) and MSPDI
251
+ # PercentComplete is the same 0-100 scale, so emit it as-is. The earlier
252
+ # `* 100` turned a native 50% task into an invalid PercentComplete=5000.
253
+ _sub_text(task_el, "PercentComplete", str(round(task.percent_complete or 0)))
254
+ if task.notes:
255
+ _sub_text(task_el, "Notes", task.notes)
256
+ _add_task_dates(task_el, task, day_end_time)
257
+
258
+ # PERT three-point per-task values. Only emitted for the leaf, non-milestone
259
+ # tasks selected by _select_pert_tasks. Duration4 is the PERT-Expected
260
+ # formula slot; MS Project derives it on read from the project-level Formula
261
+ # definition, so we never emit a per-task Duration4 value.
262
+ if is_pert:
263
+ # All three are non-None by the _select_pert_tasks filter, but mypy
264
+ # can't carry that proof across the call boundary.
265
+ assert task.optimistic_duration is not None
266
+ assert task.most_likely_duration is not None
267
+ assert task.pessimistic_duration is not None
268
+ _add_pert_task_values(
269
+ task_el,
270
+ task.optimistic_duration,
271
+ task.most_likely_duration,
272
+ task.pessimistic_duration,
273
+ )
274
+
275
+ for pred_pk, dep_type, lag in preds:
276
+ pred_uid = task_pk_to_uid.get(pred_pk)
277
+ if pred_uid is None:
278
+ continue
279
+ pred_el = ET.SubElement(task_el, f"{{{_NS}}}PredecessorLink")
280
+ _sub_text(pred_el, "PredecessorUID", str(pred_uid))
281
+ _sub_text(pred_el, "Type", _DEP_TYPE_TO_LINK_TYPE.get(dep_type, "1"))
282
+ # LinkLag is tenths of a minute; one working day of lag is
283
+ # MSPDI_LAG_TENTHS_PER_WORKING_DAY (#2290 unit seam).
284
+ _sub_text(pred_el, "LinkLag", str(lag * MSPDI_LAG_TENTHS_PER_WORKING_DAY))
285
+ _sub_text(pred_el, "LagFormat", "7")
286
+
287
+
288
+ def _add_resources_block(
289
+ root: ET.Element, resources: list[Any], resource_pk_to_uid: dict[str, int]
290
+ ) -> None:
291
+ """Emit the ``<Resources>`` block."""
292
+ resources_el = ET.SubElement(root, f"{{{_NS}}}Resources")
293
+ for resource in resources:
294
+ uid = resource_pk_to_uid[str(resource.pk)]
295
+ res_el = ET.SubElement(resources_el, f"{{{_NS}}}Resource")
296
+ _sub_text(res_el, "UID", str(uid))
297
+ _sub_text(res_el, "Name", resource.name)
298
+ # The resource's own catalog capacity, NOT its project-effective figure
299
+ # (#3574). MSPDI has no per-project override concept, so writing the
300
+ # effective value here would bake one project's slice into the resource
301
+ # default — and the importer reads MaxUnits straight back into
302
+ # ``Resource.max_units``, so a round trip would make the override permanent
303
+ # and global. The override is a TruePPM-side fact with no MSPDI counterpart.
304
+ _sub_text(res_el, "MaxUnits", f"{float(resource.max_units):.2f}")
305
+
306
+
307
+ def _add_assignments_block(
308
+ root: ET.Element,
309
+ assignments: list[Any],
310
+ task_pk_to_uid: dict[str, int],
311
+ resource_pk_to_uid: dict[str, int],
312
+ ) -> None:
313
+ """Emit the ``<Assignments>`` block, skipping any dangling task/resource ref."""
314
+ assignments_el = ET.SubElement(root, f"{{{_NS}}}Assignments")
315
+ asgn_uid = 1
316
+ for asgn in assignments:
317
+ task_uid = task_pk_to_uid.get(str(asgn.task_id))
318
+ res_uid = resource_pk_to_uid.get(str(asgn.resource_id))
319
+ if task_uid is None or res_uid is None:
320
+ continue
321
+ asgn_el = ET.SubElement(assignments_el, f"{{{_NS}}}Assignment")
322
+ _sub_text(asgn_el, "UID", str(asgn_uid))
323
+ _sub_text(asgn_el, "TaskUID", str(task_uid))
324
+ _sub_text(asgn_el, "ResourceUID", str(res_uid))
325
+ _sub_text(asgn_el, "Units", f"{float(asgn.units):.2f}")
326
+ asgn_uid += 1
327
+
328
+
329
+ def _sub_text(parent: ET.Element, tag: str, text: str) -> ET.Element:
330
+ """Add a child element with text content."""
331
+ el = ET.SubElement(parent, f"{{{_NS}}}{tag}")
332
+ el.text = text
333
+ return el
334
+
335
+
336
+ def _format_date(d: date, time_of_day: str = "00:00:00") -> str:
337
+ """Format a date as MS Project expects: ``YYYY-MM-DDTHH:MM:SS`` (midnight by default)."""
338
+ return f"{d.isoformat()}T{time_of_day}"
339
+
340
+
341
+ def _days_to_duration(days: int) -> str:
342
+ """Convert working days to MS Project ISO 8601 duration.
343
+
344
+ Uses the nominal working-day length from the unit seam (#2290) rather than a
345
+ bare ``* 8`` so a 0.5 canonical-unit change re-homes in one place.
346
+ """
347
+ hours = days * HOURS_PER_WORKING_DAY
348
+ return f"PT{hours}H0M0S"
349
+
350
+
351
+ def _minutes_to_time(minutes: int) -> str:
352
+ """Format minutes-since-midnight as an MSPDI time-of-day (``HH:MM:00``)."""
353
+ return f"{minutes // 60:02d}:{minutes % 60:02d}:00"
354
+
355
+
356
+ def _add_calendars_block(root: ET.Element, applied_calendars: list[Any]) -> None:
357
+ """Emit ``<Calendars>`` with one base calendar folding every applied layer (#1769).
358
+
359
+ MSPDI has no equivalent of TruePPM's composable calendar layers (#906,
360
+ ADR-0251), so the export flattens them exactly the way the CPM pass composes
361
+ them: weekday masks AND-ed, exception ranges unioned, ``hours_per_day`` and
362
+ name from the base (first) calendar. The exported file then schedules on the
363
+ same non-working mask TruePPM computed, at the cost of the layer structure.
364
+
365
+ TruePPM stores only a daily hour total, not shift times, so each working day
366
+ is synthesized as a single continuous shift starting at 08:00 (falling back
367
+ to midnight when the total would run past midnight). The caller must invoke
368
+ this before the ``<Tasks>`` block is appended — ``ET.SubElement`` appends at
369
+ call time, so call ordering is the positional guarantee (same contract as
370
+ ``_add_pert_extended_attribute_defs``).
371
+ """
372
+ base = applied_calendars[0]
373
+ mask, merged_exceptions = _merge_applied_calendars(applied_calendars)
374
+
375
+ cals_el = ET.SubElement(root, f"{{{_NS}}}Calendars")
376
+ cal_el = ET.SubElement(cals_el, f"{{{_NS}}}Calendar")
377
+ _sub_text(cal_el, "UID", "1")
378
+ _sub_text(cal_el, "Name", base.name)
379
+ _sub_text(cal_el, "IsBaseCalendar", "1")
380
+ _sub_text(cal_el, "BaseCalendarUID", "-1")
381
+
382
+ start_minute, shift_minutes = _shift_window(base.hours_per_day)
383
+ _add_weekday_elements(cal_el, mask, start_minute, shift_minutes)
384
+ if merged_exceptions:
385
+ _add_calendar_exception_elements(cal_el, merged_exceptions)
386
+
387
+
388
+ def _shift_window(hours_per_day: float) -> tuple[int, int]:
389
+ """``(start_minute, shift_minutes)`` of the one synthesized daily shift.
390
+
391
+ 08:00 plus the daily hour total, falling back to a midnight start when that
392
+ would run past midnight. Shared by the ``<Calendars>`` block and the
393
+ end-of-day milestone time (#4079) so the two can never disagree.
394
+ """
395
+ start_minute = 8 * 60
396
+ shift_minutes = max(0, min(round(hours_per_day * 60), 24 * 60))
397
+ if start_minute + shift_minutes > 24 * 60:
398
+ start_minute = 0
399
+ return start_minute, shift_minutes
400
+
401
+
402
+ # MS Project's built-in "Standard" calendar works 08:00-12:00 and 13:00-17:00.
403
+ _MSP_STANDARD_DAY_END = "17:00:00"
404
+ # The last representable time of day. ``T24:00:00`` is not a valid xs:dateTime
405
+ # for MS Project, which would reject or misplace the milestone.
406
+ _LAST_MINUTE_OF_DAY = 24 * 60 - 1
407
+
408
+
409
+ def _milestone_day_end_time(applied_calendars: list[Any]) -> str:
410
+ """The time of day an end-of-day milestone is written at (#4079).
411
+
412
+ It must be the end of the working day of the calendar MS Project will actually
413
+ schedule the file on. With an applied calendar that is the synthesized shift
414
+ the ``<Calendars>`` block declares (:func:`_shift_window`). With none, no
415
+ ``<Calendars>`` block is emitted and MS Project falls back to its own Standard
416
+ calendar, whose day ends at 17:00 — not at the 16:00 the engine's 8-hour
417
+ default would give from an 08:00 start. A shift that runs to midnight
418
+ (``hours_per_day`` >= 16 makes it start at 00:00 and end at 24:00, or a
419
+ 24-hour day) is clamped to 23:59, the last time of day xs:dateTime can say.
420
+ """
421
+ if not applied_calendars:
422
+ return _MSP_STANDARD_DAY_END
423
+ start_minute, shift_minutes = _shift_window(applied_calendars[0].hours_per_day)
424
+ return _minutes_to_time(min(start_minute + shift_minutes, _LAST_MINUTE_OF_DAY))
425
+
426
+
427
+ def _merge_applied_calendars(
428
+ applied_calendars: list[Any],
429
+ ) -> tuple[int, dict[tuple[Any, Any], str]]:
430
+ """Fold the applied calendar layers into one working mask and exception set.
431
+
432
+ Weekday masks are AND-ed and exception ranges unioned, matching how the CPM
433
+ pass composes layers. First writer wins on a duplicate range so the base
434
+ calendar's description (not an overlay's) labels the exception.
435
+ """
436
+ mask = applied_calendars[0].working_days
437
+ merged_exceptions: dict[tuple[Any, Any], str] = {}
438
+ for cal in applied_calendars:
439
+ mask &= cal.working_days
440
+ for exc in cal.exceptions.all():
441
+ merged_exceptions.setdefault((exc.exc_start, exc.exc_end), exc.description)
442
+ return mask, merged_exceptions
443
+
444
+
445
+ def _add_weekday_elements(
446
+ cal_el: ET.Element, mask: int, start_minute: int, shift_minutes: int
447
+ ) -> None:
448
+ """Emit the seven ``<WeekDay>`` entries, each with a single synthesized shift."""
449
+ weekdays_el = ET.SubElement(cal_el, f"{{{_NS}}}WeekDays")
450
+ for day_type, bit in _DAY_TYPES_AND_BITS:
451
+ day_el = ET.SubElement(weekdays_el, f"{{{_NS}}}WeekDay")
452
+ _sub_text(day_el, "DayType", day_type)
453
+ working = bool(mask & bit)
454
+ _sub_text(day_el, "DayWorking", "1" if working else "0")
455
+ if working and shift_minutes > 0:
456
+ times_el = ET.SubElement(day_el, f"{{{_NS}}}WorkingTimes")
457
+ wt_el = ET.SubElement(times_el, f"{{{_NS}}}WorkingTime")
458
+ _sub_text(wt_el, "FromTime", _minutes_to_time(start_minute))
459
+ _sub_text(wt_el, "ToTime", _minutes_to_time(start_minute + shift_minutes))
460
+
461
+
462
+ def _add_calendar_exception_elements(
463
+ cal_el: ET.Element, merged_exceptions: dict[tuple[Any, Any], str]
464
+ ) -> None:
465
+ """Emit the ``<Exceptions>`` block for the merged non-working date ranges."""
466
+ excs_el = ET.SubElement(cal_el, f"{{{_NS}}}Exceptions")
467
+ for (exc_start, exc_end), description in sorted(merged_exceptions.items()):
468
+ exc_el = ET.SubElement(excs_el, f"{{{_NS}}}Exception")
469
+ period_el = ET.SubElement(exc_el, f"{{{_NS}}}TimePeriod")
470
+ _sub_text(period_el, "FromDate", f"{exc_start.isoformat()}T00:00:00")
471
+ _sub_text(period_el, "ToDate", f"{exc_end.isoformat()}T23:59:00")
472
+ if description:
473
+ _sub_text(exc_el, "Name", description)
474
+ _sub_text(exc_el, "DayWorking", "0")
475
+
476
+
477
+ def _add_summary_task(tasks_el: ET.Element, project: object) -> None:
478
+ """Add the UID 0 project summary task."""
479
+ task_el = ET.SubElement(tasks_el, f"{{{_NS}}}Task")
480
+ _sub_text(task_el, "UID", "0")
481
+ _sub_text(task_el, "Name", project.name) # type: ignore[attr-defined]
482
+ _sub_text(task_el, "OutlineLevel", "0")
483
+ _sub_text(task_el, "OutlineNumber", "0")
484
+
485
+
486
+ def _summary_pks_from_tasks(tasks: list[Any]) -> set[str]:
487
+ """Return the set of task primary keys that are summary tasks.
488
+
489
+ A task is a summary when another task's ``wbs_path`` is strictly descended
490
+ from this one's (e.g. ``"1"`` is a summary if any task has path ``"1.1"``).
491
+ O(N) over the task list: build the set of strict ancestor paths from every
492
+ leaf path in one pass, then mark every task whose own path is in that set.
493
+ """
494
+ ancestor_paths: set[str] = set()
495
+ for t in tasks:
496
+ path = getattr(t, "wbs_path", None)
497
+ if not path:
498
+ continue
499
+ path = str(path)
500
+ parts = path.split(".")
501
+ for end in range(1, len(parts)):
502
+ ancestor_paths.add(".".join(parts[:end]))
503
+ return {
504
+ str(t.pk)
505
+ for t in tasks
506
+ if getattr(t, "wbs_path", None) and str(t.wbs_path) in ancestor_paths
507
+ }
508
+
509
+
510
+ def _add_pert_extended_attribute_defs(root: ET.Element) -> None:
511
+ """Emit the four PERT ExtendedAttribute definitions at project level.
512
+
513
+ Duration1–3 carry the aliases Optimistic / Most Likely / Pessimistic.
514
+ Duration4 carries the formula MS Project uses to derive PERT Expected
515
+ on file open. The caller is responsible for invoking this **before** the
516
+ ``<Tasks>`` block is appended, so the resulting element order
517
+ (``<ExtendedAttributes>`` then ``<Tasks>``) matches MS Project's own
518
+ element order. ``ET.SubElement`` appends at call time, so call ordering
519
+ is the positional guarantee — don't move this call after ``<Tasks>``.
520
+ """
521
+ ea_block = ET.SubElement(root, f"{{{_NS}}}ExtendedAttributes")
522
+ for fid in (DURATION1_FIELD_ID, DURATION2_FIELD_ID, DURATION3_FIELD_ID, DURATION4_FIELD_ID):
523
+ ea = ET.SubElement(ea_block, f"{{{_NS}}}ExtendedAttribute")
524
+ _sub_text(ea, "FieldID", fid)
525
+ _sub_text(ea, "FieldName", PERT_FIELD_NAMES[fid])
526
+ _sub_text(ea, "Alias", PERT_ALIAS_LABELS[fid])
527
+ if fid == DURATION4_FIELD_ID:
528
+ _sub_text(ea, "Formula", PERT_EXPECTED_FORMULA)
529
+
530
+
531
+ def _add_pert_task_values(
532
+ task_el: ET.Element,
533
+ optimistic_days: int,
534
+ most_likely_days: int,
535
+ pessimistic_days: int,
536
+ ) -> None:
537
+ """Emit per-task <ExtendedAttribute> values for Duration1/2/3.
538
+
539
+ Values are encoded as ISO-8601 ``PT{hours}H0M0S`` (8h working day) using
540
+ the same convention as the primary Duration field. ``DurationFormat=7``
541
+ marks the value as a duration in days for MS Project's UI.
542
+ """
543
+ for fid, days in (
544
+ (DURATION1_FIELD_ID, optimistic_days),
545
+ (DURATION2_FIELD_ID, most_likely_days),
546
+ (DURATION3_FIELD_ID, pessimistic_days),
547
+ ):
548
+ ea = ET.SubElement(task_el, f"{{{_NS}}}ExtendedAttribute")
549
+ _sub_text(ea, "FieldID", fid)
550
+ _sub_text(ea, "Value", _days_to_duration(days))
551
+ _sub_text(ea, "DurationFormat", "7")
@@ -0,0 +1,77 @@
1
+ """MS Project ExtendedAttribute FieldID constants and PERT mapping.
2
+
3
+ These FieldIDs are the canonical MS Project Custom Field identifiers for the
4
+ idiomatic three-point / PERT estimate convention. They are sourced from MPXJ's
5
+ ``TaskField`` enum (via the mpp-sample-generator's ``field_id_reference.md``)
6
+ and cross-checked against Microsoft's one documented anchor:
7
+ ``pjCustomTaskText1 = 188743731``
8
+ (https://learn.microsoft.com/en-us/previous-versions/office/developer/office-2007/bb968474(v=office.12)).
9
+
10
+ The Duration family is **non-contiguous** within the Custom Field enumeration:
11
+ ``Duration1``–``Duration3`` sit at 188743783–785 (contiguous), but ``Duration4``
12
+ (the PERT-Expected formula slot) lives 170 IDs higher at 188743955, in a
13
+ separate sub-range. Inferring one Duration FieldID by offset arithmetic from
14
+ another is wrong. Use these named constants — never a range.
15
+
16
+ ADR-0093 covers the import/export mapping decisions (alias-vs-FieldID
17
+ precedence, all-or-none rule, summary/milestone skipping, round-trip
18
+ tolerance).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ # Locked FieldIDs (sourced, not guessed). Strings because MSPDI carries them
24
+ # as text and we compare against text from the parsed XML.
25
+ DURATION1_FIELD_ID = "188743783" # Optimistic
26
+ DURATION2_FIELD_ID = "188743784" # Most Likely
27
+ DURATION3_FIELD_ID = "188743785" # Pessimistic
28
+ DURATION4_FIELD_ID = "188743955" # PERT Expected (formula slot, not imported)
29
+
30
+ # Semantic role -> FieldID. Duration4 is informational only: the formula slot
31
+ # is derived from the other three at file-read time by MS Project, so the
32
+ # importer never reads its value. The exporter still emits the definition so
33
+ # Project knows the alias and formula.
34
+ PERT_FIELD_IDS: dict[str, str] = {
35
+ "optimistic": DURATION1_FIELD_ID,
36
+ "most_likely": DURATION2_FIELD_ID,
37
+ "pessimistic": DURATION3_FIELD_ID,
38
+ "expected": DURATION4_FIELD_ID,
39
+ }
40
+
41
+ # FieldID -> the role it should play (used during import to recognize standard
42
+ # definitions).
43
+ PERT_ROLE_BY_FIELD_ID: dict[str, str] = {v: k for k, v in PERT_FIELD_IDS.items()}
44
+
45
+ # Canonical alias labels emitted on export.
46
+ PERT_ALIAS_LABELS: dict[str, str] = {
47
+ DURATION1_FIELD_ID: "Optimistic",
48
+ DURATION2_FIELD_ID: "Most Likely",
49
+ DURATION3_FIELD_ID: "Pessimistic",
50
+ DURATION4_FIELD_ID: "PERT Expected",
51
+ }
52
+
53
+ # Canonical FieldName labels (Duration1..4). MS Project uses these as the
54
+ # native field identifiers; the Alias is the user-visible label.
55
+ PERT_FIELD_NAMES: dict[str, str] = {
56
+ DURATION1_FIELD_ID: "Duration1",
57
+ DURATION2_FIELD_ID: "Duration2",
58
+ DURATION3_FIELD_ID: "Duration3",
59
+ DURATION4_FIELD_ID: "Duration4",
60
+ }
61
+
62
+ # Duration4 carries the PERT expected formula so MS Project re-derives the
63
+ # value on file open. Not a TruePPM field.
64
+ PERT_EXPECTED_FORMULA = "([Duration1] + 4*[Duration2] + [Duration3]) / 6"
65
+
66
+ # Substring tokens for alias confirmation (case-insensitive, first-match).
67
+ # These let us spot when a file has repurposed Duration1/2/3 with an alias
68
+ # that explicitly contradicts the PERT role (e.g. Duration1 aliased
69
+ # "Risk Score") and refuse the import for that field rather than silently
70
+ # treat unrelated values as estimates. Empty / missing aliases are treated
71
+ # as "no contradiction" — we trust the FieldID.
72
+ PERT_ALIAS_TOKENS: dict[str, tuple[str, ...]] = {
73
+ DURATION1_FIELD_ID: ("optimistic",),
74
+ DURATION2_FIELD_ID: ("most likely", "most-likely"),
75
+ DURATION3_FIELD_ID: ("pessimistic",),
76
+ DURATION4_FIELD_ID: ("pert", "expected"),
77
+ }