Skip to content

[OU-ADD] project: migrate collaborator access and stage duration - #1

Open
adekock11 wants to merge 2 commits into
20.0from
cursor/project-20-migration-2b2f
Open

adekock11 wants to merge 2 commits into
20.0from
cursor/project-20-migration-2b2f

Conversation

@adekock11

@adekock11 adekock11 commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Migration scripts for the project family from Odoo 19.0 to 20.0, on top of the generated analysis in OCA/OpenUpgrade#6068 (head hbrunn/OpenUpgrade@2d9eb6bab7e7e579953e38d7ad3b96aad6fcc44d).

This branch does not cherry-pick the OCA#6068 commits. It copies that PR's upgrade_analysis.txt / noupdate_changes.xml for the modules below, and it copies docsource/modules190-200.rst so the status cells can be filled. If OCA#6068 lands first, rebase this branch: the coverage table should shrink to the four status cells this PR changes (project, project_todo, project_sms, project_mail_plugin).

Module Status What the script does
project 20.0.1.4 Done Collaborator access, team members, stage duration, email_cc, noupdate mail/security
project_todo 20.0.1.0 Done Reloads the noupdate onboarding qweb
project_sms 20.0.1.1 Nothing to do ir.access rename only, handled by base
project_mail_plugin 20.0.1.0 Nothing to do One non-noupdate action removed with the views

Runtime dependency: base must already have merged ir.model.access and ir.rule into ir.access (the base scripts in OCA#6068, Odoo commit e7cc76b290). The project post-migration loads a noupdate ir.access record and assumes that merge has run. openupgrade_framework from the same PR is what actually executes these scripts.

project

Analysis source: openupgrade_scripts/scripts/project/20.0.1.4/upgrade_analysis.txt (unchanged from OCA#6068). Annotations are in upgrade_analysis_work.txt.

project.collaborator.limited_access deleted, access_mode required

Odoo commits a59b237f77 and 04e9a2509d (task 6479946 / 6479918, PR odoo/odoo#284245).

v19 stored a boolean. The share wizard exposed it as:

  • limited_access false, label "Edit": the portal user could write every task, including stage_id and priority.
  • limited_access true, label "Edit with limited access": the portal user could write tasks they followed. Visibility was row-level and tied to followers.
  • Followers who were not collaborators were "Read". They were not rows in project.collaborator. The portal project rule was message_partner_ids child_of the user's commercial partner.

v20 replaces that with a required selection view / edit / advanced_edit. Read access no longer depends on following a task. edit may change ordinary fields and may not change stage or priority. advanced_edit may change stage and priority. The new default is view.

Without a script, every existing collaborator would become view (the column default) and every portal user who could open a project only because they followed it would lose the project. The v20 portal project rule requires a collaborator row.

Pre-migration, before the ORM adds the required column:

  • limited_access false, and NULL (the old default was false) → advanced_edit
  • limited_access true → edit
  • Share partners who follow a portal or invited_users project and are not already collaborators → new row access_mode = view
  • The same view row for every existing portal user of that follower's commercial entity, so the old child_of commercial-partner rule still holds

edit is a behaviour change for old limited users: they can now see every task, and they can no longer move stage or priority. That is the split Odoo describes in 04e9a2509d. Keeping them on advanced_edit would have preserved stage writes and also given them every task, which is a larger grant than v19. The full "Edit" users go to advanced_edit so they keep stage and priority.

project.project.allowed_internal_user_ids (Team Members)

Odoo commit bae4e441d9.

v19 project_public_members_rule let any internal user read a project when they followed it. v20 reads allowed_internal_user_ids and ignores followers. employees and portal projects stay visible to every internal user. followers and invited_users projects become invisible to everyone except the team members and the project admin group.

The new field is a stored compute that only adds user_id (the project manager) and then filters by company. Letting that compute run on an empty relation would drop every other internal follower.

Pre-migration creates project_allowed_internal_users_rel (columns project_project_id, res_users_id, the names Odoo assigns when the m2m declaration omits them) and fills it:

  • internal (share is false) followers of followers / invited_users projects, when the project has no company or the user belongs to that company
  • the project manager under the same company rule

Many2many.update_db queues the stored compute only when the relation table does not exist yet. Creating the table first stops that compute from replacing the copied followers with the manager alone. Foreign keys are still added by the ORM's post_init.

duration_tracking is now stored, and project.project.date_last_stage_update is new

Odoo commit d5e8a68f4b (mail.tracking.duration.mixin, also project).

v19 computed duration_tracking on the fly from mail.tracking.value as {stage_id: seconds}, including the still-open visit. v20 stores JSON and the compute only continues from what is already stored:

{"d": "<UTC datetime the current stage was entered>",
 "s": <stage id or 0>,
 "<stage id>": <minutes spent on previous visits>}

The burndown and time-by-stage SQL add the open visit from d at read time, in minutes. An empty column plus the new compute would record "entered the current stage during the upgrade" and drop history.

Pre-migration adds the jsonb column (so _init_column_data does not recompute it: the field is not required and the column is already non-null) and fills it from mail.tracking.value for stage_id on project.task and project.project:

  • intervals are ordered by mail_tracking_value.id
  • minutes are floor(epoch / 60) between tracking dates, starting from create_date
  • repeated visits to the same stage are summed
  • the open visit is not stored
  • s is the current stage_id or 0
  • d is the last tracking date, else create_date

date_last_stage_update on project.project uses that same timestamp. The field default is now(), which would mark every project as just moved and would skew rotting. Tasks already have this column from v19; it is left alone.

email_cc removed from project.task and project.update

Confirmed on odoo/odoo 20.0 (d7014bd26978) against 19.0.

odoo/odoo@3c26c0553754 ([IMP] mail,*: remove MailThreadCC, part of odoo/odoo#238694) deletes mail.thread.cc. The mixin was the only definition of the stored email_cc char. 20.0 addons/mail has no mail.thread.cc model.

Who had the column in 19:

  • project.task inherits mail.thread.cc and redeclares email_cc (project/models/project_task.py). The gateway writes unmatched CC addresses there (message_new). 20.0 project.task inherits mail.thread via mail.tracking.duration.mixin and does not declare email_cc.
  • project.update inherits mail.thread.cc in 19 and mail.thread in 20. It never redeclared the field, so the column exists only because of the mixin. 20.0 project.update has description and no email_cc.

project.project never inherited the mixin, so it has no email_cc column.

ir.model.fields._reflect_fields upserts fields that still exist. It does not unlink a base field that left the registry, and it does not DROP COLUMN. _drop_column runs only from ir.model.fields.unlink. After the upgrade the form no longer shows the field, and the addresses sit in project_task.email_cc / project_update.email_cc with nothing reading them. mail.message.incoming_email_cc still holds the CC of each message, which is not the accumulated list the document field kept for the composer.

Same preservation as PR #2 for crm.lead (that PR's pre-migration), because both models lost the mixin and both keep an Html description:

Pre-migration, while the column exists:

  • Skip blank and whitespace-only values.
  • HTML-escape &, < and > (so Ann <a@x.com> & Bob cannot break the description).
  • Append <p>Email cc preserved from previous version: …</p>. A non-empty description gets a <br/> before that paragraph. An empty description gets only the paragraph.
  • strpos skips a row whose description already contains that sentence, so a crash between the UPDATE and the DROP does not append twice.
  • DROP COLUMN. A later run sees no column and returns. _drop_column also checks column_exists before dropping, so a later field unlink does not fail on the missing column.

There is no other v20 field for these addresses. New mail subscribes partners it can match and does not store the unmatched list on the task.

Fields with nothing to do

  • website_message_ids removed on milestone, project, task and project update. It is a non-stored one2many from portal's mail.thread inherit (overridden on project.task). v20 portal drops the field. Messages stay on mail.message. No column. The portal migration owns the field removal.
  • project.project.stage.color and project.task.type.color deleted. Cosmetic kanban index, no replacement field. The integers are dropped.
  • project.project.stage.rotting_threshold_days new, default 0 (rotting off). No v19 source.
  • project.role.user_ids / res.users.project_role_ids: one new m2m. v19 roles had no members.
  • project.task.allow_recurring_tasks and allow_task_dependencies are no longer stored related fields. They are non-stored computes: the project flag, or is_template when the task has no project. The ORM drops the obsolete columns. Template tasks without a project will now read True instead of the old related False.
  • project.update._order is date desc, id desc. Ordering only. date already exists.
  • res.users.followed_project_ids is a non-stored search over mail.followers.
  • New SQL view project.task.stage.report (odoo/odoo@eb9e98c656). The ORM creates it.
  • constraint_project_task_private_task_has_no_parent was relaxed to allow template subtasks. Rows that passed the old CHECK pass the new one.

XML

ir.model.access and ir.rule lines in the analysis are the base rename to ir.access (odoo/odoo@e7cc76b290). Standard XML is reloaded from ir.access.csv. The domains that now mention allowed_internal_user_ids and access_mode become true because of the pre-migration above.

Post-migration loads noupdate_changes.xml (generated by the analysis, unchanged):

  • mail_template_data_project_task and rating_project_request_email_template gain email_layout_xmlid = mail.mail_notification_light. body_html is not in the file, so a customized body stays.
  • project_message_user_assigned: t-esc becomes t-out. The sentence is the same. A customized arch of this noupdate qweb is overwritten. t-esc still parses in 20.0, so leaving a translated arch alone would not crash.
  • project_task_rule_portal_project_sharing: domain requires access_mode in ('edit', 'advanced_edit'), operation cu, group portal. Loaded after base has converted the old ir.rule to ir.access.

New views, menus, server actions, digest tips and portal.entry rows are module data. Deleted views were not noupdate.

project_todo

No model or field changes. The ir.access / ir.rule lines are the same base rename. task_edition_rule_internal keeps its xmlid, domain and crud operation, so the noupdate flag does not freeze a stale rule.

Post-migration reloads noupdate qweb todo_user_onboarding. Upstream wrapped the shortcut block in <t name="shortcut_section"> and set width: 50% on the two screenshots. The wording is unchanged. A database that edited this onboarding HTML gets the upstream arch back.

project_sms

Nothing to do. The only analysis lines are ir.model.access / ir.rule becoming ir.access, with no noupdate domain diff. Base preserves manual rows.

project_mail_plugin

Nothing to do. project_task_action_form_edit was not noupdate. The module no longer ships project_client.py or project_task_views.xml. The module update deletes the xmlid. No stored business data.

Left for other PRs

These have real 19→20 changes and should not ride along with project:

  • hr_timesheet (own PR). account.analytic.line.sequence is new. portal_timesheet_ids / show_portal_timesheets are new. The analysis reports business_domain losing the purchase_order key. The noupdate portal timesheet rule changes operation from full CRUD to read and drops the message_partner_ids clause. Project's collaborator move does not make the old domain invalid: analytic lines still have followers. It does leave portal timesheet rights wider than v20 until that module is migrated.
  • hr_timesheet_attendance: ir.access rename only. Belongs with timesheets.
  • sale_project, sale_timesheet, sale_project_margin, sale_project_stock, project_sale_expense. sale_timesheet drops so_line, timesheet_invoice_id and timesheet_invoice_type, and moves order_id to sale. sale_project adds stored billable_type and category_report, and project.milestone.quantity_percentage stops being a compute. project_sale_expense adds account.analytic.line.category. That is a sale migration.
  • project_account, project_hr_skills, project_mrp, project_mrp_account, project_purchase, project_timesheet_holidays. Analysis is views, ir.access renames, or a new non-unique index (project_timesheet_holidays index on timesheet lines). No stored data to move. Their docsource rows are left blank in the copied table so the owning PR can mark them.

How it was tested

PostgreSQL 16 fixture, not a full Odoo database. The SQL in pre-migration.py was executed through openupgradelib.logged_query / add_columns against hand-built tables (project_task, project_project, project_collaborator, mail_tracking_value, mail_message, mail_followers, res_partner, res_users, res_company_users_rel, ir_model_fields).

Assertions that passed:

  • A task created 2026-01-01, stage 2, one tracking "left stage 1 on 2026-01-03" stores {"1": 2880, "s": 2, "d": "2026-01-03 ..."} (2880 minutes = 2 days). A tracking row on a different field is ignored.
  • A task that visits stage 1 for 1 day, stage 2 for 2 days, then returns to stage 1 stores previous minutes 1 → 1440, 2 → 2880, s = 1, d = the second tracking date. The open visit is not in the totals.
  • A task with no tracking stores s = 0 and d = create_date only.
  • A project that left stage 7 on 2026-03-01 stores 59 days of minutes on key "7", s = 8, and date_last_stage_update = 2026-03-01 (not now()).
  • limited_access false → advanced_edit, true → edit, NULL → advanced_edit. Running the access migration again does not overwrite a filled mode.
  • A portal project followed by a share partner gains view for that partner and for the portal user of the same commercial entity. An existing limited collaborator on that project stays edit and is not duplicated.
  • A followers project gains team members for the same-company internal follower and the manager, and not for an internal follower of another company. An employees project gains nobody. A project with no company gains the follower even when that user sits on another company. The manager of an invited_users project is inserted. The secondary index on the relation is created.
  • email_cc Ann <a@x.com> & Bob on a task with a description becomes …<br/><p>Email cc preserved from previous version: Ann &lt;a@x.com&gt; &amp; Bob</p>, and the column is dropped. Whitespace and NULL are left alone. A NULL description becomes only the paragraph. The same append runs on project_update. A second call is a no-op. Putting the column back after the append and running again does not add a second paragraph, then drops the column.

ruff check and ruff format --check (ruff 0.16.8, the pre-commit pin) pass on the Python files. Both noupdate XML files parse.

Not done: installing Odoo 19 and 20 and upgrading a demo database. The scripts were not executed by Odoo's migration loader, and noupdate_changes.xml was not loaded through openupgrade.load_data. The column-vs-recompute reasoning is from odoo/orm/fields.py update_db / _init_column_data and Many2many.update_db on odoo/odoo 20.0 d7014bd26978, not from watching a module update.

Gaps and risks

  • Old "Edit with limited access" collaborators lose stage and priority writes and gain visibility of tasks they did not follow. That matches the v20 field-level model. Databases that need the old stage right have to be moved to advanced_edit by hand.
  • Preserved email_cc is text inside the description. It is not restored as a composer CC list, and new messages do not keep writing that list. mail.message.incoming_email_cc is unchanged.
  • Stage colors on project.project.stage and project.task.type are dropped.
  • duration_tracking minutes are floored. A visit under 60 seconds stores 0 and is omitted. Tracking rows whose create_date is before create_date contribute 0.
  • Team membership is a snapshot of followers at upgrade. Later unfollows do not remove team members, which is also how v20 behaves after the split: followers and members are different lists.
  • Portal view rows are created for share followers and for portal users of the same commercial partner. A portal user of a child company with a different commercial_partner_id is not included. The v19 child_of rule was the same boundary.
  • The relation table is created without the ORM's table comment. Foreign keys are added later by update_db_foreign_keys. If a custom module already created project_allowed_internal_users_rel with different column names, the INSERT fails.
  • project_task_rule_portal_project_sharing is noupdate. Loading it overwrites a locally edited domain.
  • hr_timesheet's portal timesheet rule still uses the v19 domain until that module's own PR.
Open in Web Open in Cursor 

cursoragent and others added 2 commits October 30, 2026 20:41
Map project.collaborator.limited_access to access_mode, copy private-project
followers into team members, rebuild stored duration_tracking from mail
tracking, and refresh the project_todo onboarding template. project_sms and
project_mail_plugin have no data to move.

Co-authored-by: André de Kock <adekock11@users.noreply.github.com>
mail.thread.cc is gone in 20.0, and project.task / project.update do not
redeclare email_cc. Append the escaped addresses to description and drop
the column, matching the crm.lead migration.

Co-authored-by: André de Kock <adekock11@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants