Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,342 changes: 1,342 additions & 0 deletions docsource/modules190-200.rst

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions openupgrade_scripts/scripts/crm/20.0.1.9/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2026 Odoo Community Association (OCA)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from openupgradelib import openupgrade

# Data files that defined these records were removed from the manifest, so
# module loading does not delete them until the very end of the upgrade.
# Drop them before view validation: the wizard view targets a transient
# model that no longer exists, and the server actions call removed methods.
_DELETED_XMLIDS = [
"crm.action_crm_lead2opportunity_partner",
"crm.action_opportunity_forecast",
"crm.action_your_pipeline",
"crm.view_crm_lead2opportunity_partner",
"crm.crm_lead_all_leads_view_calendar",
]


@openupgrade.migrate()
def migrate(env, version):
openupgrade.delete_records_safely_by_xml_id(env, _DELETED_XMLIDS)
183 changes: 183 additions & 0 deletions openupgrade_scripts/scripts/crm/20.0.1.9/pre-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Copyright 2026 Odoo Community Association (OCA)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging

from openupgradelib import openupgrade

_logger = logging.getLogger(__name__)


def _preserve_email_cc(env):
"""Keep crm.lead email_cc, which mail.thread.cc used to store.

The mixin is gone in 20.0 (odoo/odoo@3c26c0553754) and nothing else
holds those addresses. Append them to the lead description, then drop
the column so it is not an invisible leftover.
"""
if not openupgrade.column_exists(env.cr, "crm_lead", "email_cc"):
return
openupgrade.logged_query(
env.cr,
"""
UPDATE crm_lead
SET description = CONCAT(
COALESCE(description, ''),
CASE
WHEN COALESCE(description, '') = '' THEN ''
ELSE '<br/>'
END,
'<p>Email cc preserved from previous version: ',
replace(
replace(replace(btrim(email_cc), '&', '&amp;'), '<', '&lt;'),
'>',
'&gt;'
),
'</p>'
)
WHERE COALESCE(btrim(email_cc), '') <> ''
""",
)
_logger.info(
"Preserved email_cc on %s crm.lead rows into description.",
env.cr.rowcount,
)
openupgrade.logged_query(env.cr, "ALTER TABLE crm_lead DROP COLUMN email_cc")


def _fill_duration_tracking(env):
"""Fill stored duration_tracking before the ORM recomputes it.

v19 rebuilt a non-stored ``{stage_id: seconds}`` map from
mail.tracking.value on every read. v20 stores
``{"s", "d", "<stage_id>": minutes}`` and the compute only rolls that
dict forward (odoo/odoo@d5e8a68f4b). Recomputing NULL keeps the current
stage and drops history. Completed visits are stored as minutes; the
open visit stays in ``d`` (date_last_stage_update), which is what the
web client adds on top of the stored map.
"""
if not openupgrade.column_exists(env.cr, "crm_lead", "duration_tracking"):
openupgrade.logged_query(
env.cr,
"ALTER TABLE crm_lead ADD COLUMN duration_tracking jsonb",
)
openupgrade.logged_query(
env.cr,
"""
WITH stage_field AS (
SELECT id
FROM ir_model_fields
WHERE model = 'crm.lead' AND name = 'stage_id'
ORDER BY id
LIMIT 1
),
changes AS (
SELECT
m.res_id,
v.id AS tracking_id,
v.create_date AS change_date,
COALESCE(v.old_value_integer, 0) AS old_stage_id
FROM mail_tracking_value v
JOIN mail_message m ON m.id = v.mail_message_id
WHERE v.field_id = (SELECT id FROM stage_field)
AND m.model = 'crm.lead'
AND m.res_id IS NOT NULL
),
ordered_changes AS (
SELECT
changes.res_id,
changes.old_stage_id,
changes.change_date,
LAG(changes.change_date) OVER (
PARTITION BY changes.res_id
ORDER BY changes.tracking_id
) AS prev_change_date
FROM changes
),
intervals AS (
SELECT
ordered_changes.res_id,
ordered_changes.old_stage_id AS stage_id,
GREATEST(
0,
FLOOR(
EXTRACT(
EPOCH FROM (
ordered_changes.change_date - COALESCE(
ordered_changes.prev_change_date,
crm_lead.create_date,
ordered_changes.change_date
)
)
) / 60
)
)::int AS minutes
FROM ordered_changes
JOIN crm_lead ON crm_lead.id = ordered_changes.res_id
),
spent AS (
SELECT res_id, jsonb_object_agg(stage_id::text, minutes) AS payload
FROM (
SELECT res_id, stage_id, SUM(minutes)::int AS minutes
FROM intervals
GROUP BY res_id, stage_id
HAVING SUM(minutes) > 0
) grouped
GROUP BY res_id
)
UPDATE crm_lead
SET duration_tracking =
COALESCE(spent.payload, '{}'::jsonb)
|| jsonb_build_object(
's', COALESCE(crm_lead.stage_id, 0),
'd', to_char(
COALESCE(
crm_lead.date_last_stage_update,
crm_lead.create_date,
timezone('UTC', now())
),
'YYYY-MM-DD HH24:MI:SS'
)
)
FROM (
SELECT id
FROM crm_lead
WHERE duration_tracking IS NULL
) todo
LEFT JOIN spent ON spent.res_id = todo.id
WHERE crm_lead.id = todo.id
""",
)


def _migrate_assignment_optout(env):
"""Map team-member opt-out onto assignment_max = 0.

v20 drops assignment_optout. ``assignment_rules`` is a non-stored
compute: max >= 30000 is unlimited, max == 0 is opt-out, anything else
is limited (odoo/odoo@553fbd1afa). Existing caps stay as "limited".
"""
if not openupgrade.column_exists(env.cr, "crm_team_member", "assignment_optout"):
return
openupgrade.logged_query(
env.cr,
"""
UPDATE crm_team_member
SET assignment_max = 0
WHERE assignment_optout
""",
)
_logger.info(
"Opted %s crm.team.member rows out of assignment (assignment_max=0).",
env.cr.rowcount,
)
openupgrade.logged_query(
env.cr,
"ALTER TABLE crm_team_member DROP COLUMN assignment_optout",
)


@openupgrade.migrate()
def migrate(env, version):
_preserve_email_cc(env)
_fill_duration_tracking(env)
_migrate_assignment_optout(env)
94 changes: 94 additions & 0 deletions openupgrade_scripts/scripts/crm/20.0.1.9/upgrade_analysis.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---Models in module 'crm'---
obsolete model crm.lead2opportunity.partner [transient]
---Fields in module 'crm'---
crm / crm.lead / commercial_partner_id (many2one): DEL relation: res.partner, stored: False
crm / crm.lead / duration_tracking (json) : is now stored
crm / crm.lead / email_cc (char) : DEL
crm / crm.lead / utm_reference (reference) : NEW
crm / crm.lead / website_message_ids (one2many): DEL relation: mail.message
crm / crm.stage / color (integer) : DEL
crm / crm.team.member / assignment_optout (boolean) : DEL
crm / crm.team.member / assignment_rules (selection) : NEW selection_keys: ['limited', 'opt-out', 'unlimited'], hasdefault: compute, stored: False
---XML records in module 'crm'---
NEW ir.access: crm.access_calendar_event_type_sale_manager
NEW ir.access: crm.access_calendar_event_type_sale_user
NEW ir.access: crm.access_crm_lead2opportunity_partner_mass
NEW ir.access: crm.access_crm_lead_lost
NEW ir.access: crm.access_crm_lead_scoring_frequency
NEW ir.access: crm.access_crm_lead_scoring_frequency_field
NEW ir.access: crm.access_crm_lead_scoring_frequency_field_system
NEW ir.access: crm.access_crm_lead_scoring_frequency_system
NEW ir.access: crm.access_crm_lost_reason_manager
NEW ir.access: crm.access_crm_lost_reason_user
NEW ir.access: crm.access_crm_merge_opportunity
NEW ir.access: crm.access_crm_stage
NEW ir.access: crm.access_crm_stage_manager
NEW ir.access: crm.access_mail_activity_plan_sale_manager
NEW ir.access: crm.access_mail_activity_plan_template_sale_manager
NEW ir.access: crm.access_mail_activity_type_sale_manager
NEW ir.access: crm.access_res_partner
NEW ir.access: crm.access_res_partner_category
NEW ir.access: crm.calendar_event_rule_employee
NEW ir.access: crm.calendar_event_rule_employee_1
NEW ir.access: crm.crm_activity_report_rule_all_activities
NEW ir.access: crm.crm_activity_report_rule_multi_company
NEW ir.access: crm.crm_activity_report_rule_personal_activities
NEW ir.access: crm.crm_lead_company_rule
NEW ir.access: crm.crm_lead_pls_update_access_system
NEW ir.access: crm.crm_recurring_plan_access_manager
NEW ir.access: crm.crm_recurring_plan_access_salesman
NEW ir.access: crm.crm_rule_all_lead
NEW ir.access: crm.crm_rule_all_lead_1
NEW ir.access: crm.crm_rule_personal_lead
NEW ir.access: crm.mail_plan_rule_group_sale_manager_lead
NEW ir.access: crm.mail_plan_templates_rule_group_sale_manager_lead
NEW ir.access: crm.res_partner_rule_edit_all
NEW ir.access: crm.res_partner_rule_user
DEL ir.actions.act_window: crm.action_crm_lead2opportunity_partner
DEL ir.actions.act_window.view: crm.crm_lead_all_leads_view_calendar
DEL ir.actions.server: crm.action_opportunity_forecast
DEL ir.actions.server: crm.action_your_pipeline
DEL ir.model.access: crm.access_calendar_event
DEL ir.model.access: crm.access_calendar_event_manager
DEL ir.model.access: crm.access_calendar_event_type_sale_manager
DEL ir.model.access: crm.access_calendar_event_type_sale_salesman
DEL ir.model.access: crm.access_calendar_event_type_sale_user
DEL ir.model.access: crm.access_crm_activity_report_salesman
DEL ir.model.access: crm.access_crm_activity_report_user
DEL ir.model.access: crm.access_crm_lead
DEL ir.model.access: crm.access_crm_lead2opportunity_partner
DEL ir.model.access: crm.access_crm_lead2opportunity_partner_mass
DEL ir.model.access: crm.access_crm_lead_lost
DEL ir.model.access: crm.access_crm_lead_manager
DEL ir.model.access: crm.access_crm_lead_scoring_frequency
DEL ir.model.access: crm.access_crm_lead_scoring_frequency_field
DEL ir.model.access: crm.access_crm_lead_scoring_frequency_field_system
DEL ir.model.access: crm.access_crm_lead_scoring_frequency_system
DEL ir.model.access: crm.access_crm_lost_reason_manager
DEL ir.model.access: crm.access_crm_lost_reason_salesman
DEL ir.model.access: crm.access_crm_lost_reason_user
DEL ir.model.access: crm.access_crm_merge_opportunity
DEL ir.model.access: crm.access_crm_stage
DEL ir.model.access: crm.access_crm_stage_manager
DEL ir.model.access: crm.access_mail_activity_plan_sale_manager
DEL ir.model.access: crm.access_mail_activity_plan_template_sale_manager
DEL ir.model.access: crm.access_mail_activity_type_sale_manager
DEL ir.model.access: crm.access_res_partner
DEL ir.model.access: crm.access_res_partner_category
DEL ir.model.access: crm.access_res_partner_category_manager
DEL ir.model.access: crm.access_res_partner_manager
DEL ir.model.access: crm.crm_lead_pls_update_access_system
DEL ir.model.access: crm.crm_recurring_plan_access_manager
DEL ir.model.access: crm.crm_recurring_plan_access_salesman
DEL ir.rule: crm.crm_activity_report_rule_all_activities (noupdate)
DEL ir.rule: crm.crm_activity_report_rule_multi_company (noupdate)
DEL ir.rule: crm.crm_activity_report_rule_personal_activities (noupdate)
DEL ir.rule: crm.crm_lead_company_rule (noupdate)
DEL ir.rule: crm.crm_rule_all_lead (noupdate)
DEL ir.rule: crm.crm_rule_personal_lead (noupdate)
DEL ir.rule: crm.mail_plan_rule_group_sale_manager_lead (noupdate)
DEL ir.rule: crm.mail_plan_templates_rule_group_sale_manager_lead (noupdate)
NEW ir.ui.view: crm.crm_lead_creation_message
NEW ir.ui.view: crm.crm_lead_view_list_simplified
NEW ir.ui.view: crm.crm_module_access_request_template
DEL ir.ui.view: crm.view_crm_lead2opportunity_partner
Loading