diff --git a/automation_script/industry_automation.py b/automation_script/industry_automation.py
index 0e7d17b142..5174d0a017 100644
--- a/automation_script/industry_automation.py
+++ b/automation_script/industry_automation.py
@@ -12,11 +12,13 @@
import shutil
import argparse
import sys
+import psycopg2
from pathlib import Path
import re
from ast import literal_eval
from lxml import etree
+from lxml import html
# Setup logger
# Create a Logger instance directly
@@ -221,6 +223,7 @@ def export_studio_customizations(self, port, db_name, studio_zip_path):
_logger.error("Invalid module data: %s", module)
raise Exception("Invalid module data for web_studio")
+
if state == "uninstalled":
self.install_web_studio(port, db_name, uid, model_id)
# optional: re-check status after install
@@ -307,6 +310,76 @@ def delete_temp_dir(self, dir_path):
shutil.rmtree(dir_path)
def check_web_studio_installed(self, port, db_name, uid):
+ # Search AI module
+ search_payload = {
+ "jsonrpc": "2.0",
+ "method": "call",
+ "params": {
+ "service": "object",
+ "method": "execute_kw",
+ "args": [
+ db_name,
+ uid,
+ PASSWORD,
+ "ir.module.module",
+ "search_read",
+ [[("name", "=", "ai")]],
+ {
+ "fields": ["id", "state"],
+ "limit": 1
+ }
+ ]
+ },
+ "id": 1
+ }
+
+ response = requests.post(
+ f"{BASE_URL}{port}/jsonrpc",
+ json=search_payload
+ ).json()
+
+ if "error" in response:
+ raise Exception(
+ f"Failed to search ai module: {response['error']}"
+ )
+
+ ai_module = response.get("result", [])
+
+ # Uninstall AI module if installed
+ if ai_module and ai_module[0]["state"] == "installed":
+
+ ai_module_id = ai_module[0]["id"]
+
+ uninstall_payload = {
+ "jsonrpc": "2.0",
+ "method": "call",
+ "params": {
+ "service": "object",
+ "method": "execute_kw",
+ "args": [
+ db_name,
+ uid,
+ PASSWORD,
+ "ir.module.module",
+ "button_immediate_uninstall",
+ [[ai_module_id]]
+ ]
+ },
+ "id": 2
+ }
+
+ uninstall_response = requests.post(
+ f"{BASE_URL}{port}/jsonrpc",
+ json=uninstall_payload
+ ).json()
+
+ if "error" in uninstall_response:
+ raise Exception(
+ f"Failed to uninstall ai module: {uninstall_response['error']}"
+ )
+
+ _logger.info("AI module uninstalled successfully.")
+
# Prepare the JSON-RPC payload to search for the 'web_studio' module
payload = {
"jsonrpc": "2.0",
@@ -394,12 +467,12 @@ def __init__(self, ind_name, ind_category, db_name, module_path, port, destinati
'demo/website_page.xml',
'demo/account_analytic_plan.xml',
'demo/crm_team.xml',
- 'data/uom.uom.xml',
+ 'data/uom_uom.xml',
]
self.mandatory_files = {
"/data/mail_message.xml": """
-
+
discuss.channel
email
@@ -408,7 +481,7 @@ def __init__(self, ind_name, ind_category, db_name, module_path, port, destinati
🚀 Get started with Odoo {Ind_name}
@@ -551,7 +624,6 @@ def clean(self):
'sale_async_emails',
'snailmail_account',
'web_grid',
- 'web_studio',
'social_push_notifications',
'appointment_sms',
'website_knowledge',
@@ -587,6 +659,9 @@ def clean(self):
# Extract relevant SCSS customization data
elif current_dir.endswith('/ir_attachment/') and ext == "scss":
self.get_relevant_scss_data(scss_content_list, root, file_name)
+
+ if not scss_content_list:
+ self.get_relevant_scss_data_from_db(scss_content_list)
# Generate SCSS function from collected theme data
self.write_scss_function(destination_module_path, scss_content_list)
@@ -608,11 +683,14 @@ def clean(self):
# Retain only welcome article in the knowledge article
self.clean_knowledge_article(destination_module_path)
+ # Transform knowledge article by extracting HTML content into a QWeb template
+ self.transform_knowledge_article(destination_module_path)
+
# Add demo payment provider if relevant module is present
self.add_demo_payment_provider(destination_module_path, manifest_demo_file_list)
- # Add immediate install function for the theme module in demo XML files
- self.add_theme_immediate_install_function(destination_module_path)
+ # Add theme selection function using button_choose_theme in demo XML
+ self.add_button_choose_theme_function(destination_module_path)
# Clean up sale order line records
self.clean_sale_order_line_record(destination_module_path)
@@ -629,6 +707,9 @@ def clean(self):
# Clean up pos.payment.method records
self.clean_pos_payment_method(destination_module_path)
+ # Clean and edit homepage view(ir_ui_view)
+ self.clean_homepage_view(destination_module_path)
+
# Update demo file order in manifest
self.arrange_manifest_file(destination_module_path, manifest_demo_file_list)
@@ -972,6 +1053,70 @@ def remove_computed_fields(self, fields_info_dict, model_name, record, content):
return content
+ def get_relevant_scss_data_from_db(self, scss_content_list):
+ conn = psycopg2.connect(
+ dbname=self.db_name,
+ )
+
+ cr = conn.cursor()
+
+ query = """
+ SELECT url, index_content
+ FROM ir_attachment
+ WHERE name IN (
+ 'user_color_palette.scss',
+ 'user_values.scss',
+ 'user_theme_color_palette.scss'
+ )
+ """
+
+ cr.execute(query)
+
+ records = cr.fetchall()
+
+ scss_pattern = re.compile(r'o-map-omit\(\(\s*(.*?)\s*(?://\s*--\s*hook\s*--)?\s*\)\)', re.DOTALL)
+ for file_name, raw in records:
+ clean_url = "/website/" + file_name.split('website/')[1]
+
+ scss_content_dict = {}
+
+ scss_content = raw.decode() if isinstance(raw, bytes) else raw
+
+ scss_match = scss_pattern.search(scss_content)
+
+ if scss_match:
+ lines = []
+
+ for line in scss_match.group(1).splitlines():
+ if not line:
+ continue
+
+ line = line.strip()
+
+ if ":" in line:
+ key, value = line.split(":", 1)
+
+ value = value.strip().rstrip(",")
+ if value == "NULL":
+ value = "null"
+ if not (value.startswith("'") and value.endswith("'")):
+ value = f"'{value}'"
+
+ line = f"{key}: {value},"
+
+ lines.append(line)
+
+ inner_scss_content = "".join(lines)
+
+ scss_content_dict['url'] = clean_url
+ scss_content_dict['inner_scss_content'] = inner_scss_content
+
+ scss_content_list.append(scss_content_dict)
+
+
+ cr.close()
+ conn.close()
+
def get_relevant_scss_data(self, scss_content_list, root, file_name):
"""
Parses the given SCSS file to extract relevant customization data.
@@ -1029,10 +1174,10 @@ def write_scss_function(self, destination_module_path, scss_content_list):
for item in scss_content_list:
new_function += f"""
-
-
+
+
"""
@@ -1183,6 +1328,10 @@ def remove_unused_ir_attachment_post(self, destination_module_path):
record.remove(res_model[0])
if website_id:
record.remove(website_id[0])
+ existing = record.xpath("./field[@name='public']")
+ if not existing:
+ new_field = etree.Element("field", name="public", eval="True")
+ record.append(new_field)
if key_field or name_field:
# check key or name in ir_ui_view.xml file if not found store in list
key = key_field[0].text if key_field else None
@@ -1344,6 +1493,10 @@ def clean_knowledge_article(self, destination_module_path):
if not record.xpath('.//field[@name="is_locked"]'):
new_field = etree.Element("field", name="is_locked", eval="True")
record.append(new_field)
+
+ # Remove 'category' field if exists
+ if category_fields := record.xpath('.//field[@name="category"]'):
+ record.remove(category_fields[0])
else:
# Remove all other records
@@ -1352,6 +1505,46 @@ def clean_knowledge_article(self, destination_module_path):
self.write_etree_content(path_knowledge_article, root_knowledge_article)
return
+ def transform_knowledge_article(self, destination_module_path):
+ file_path = Path(destination_module_path + '/data/knowledge_article.xml')
+
+ if not file_path.exists():
+ return
+
+ parser = etree.XMLParser(remove_blank_text=True)
+ tree = etree.parse(file_path, parser)
+ root = tree.getroot()
+ bodyf = root.xpath("//record[@id='welcome_article']/field[@name='body']")
+ if bodyf is None:
+ return
+ cnt = 0
+ for body in bodyf:
+
+ if body.text:
+ extracted_content = body.text
+ body.text = ""
+ template = etree.Element("template", id="welcome_article_body")
+ wrapper = html.fragment_fromstring(extracted_content, create_parent=True)
+
+ # Remove attribute data-heading-link-id
+ for el in wrapper.xpath('//*[@data-heading-link-id]'):
+ del el.attrib['data-heading-link-id']
+
+ # Append all children
+ template.extend(wrapper)
+
+ root.insert(cnt, template)
+ cnt+=2
+ else:
+ continue
+
+ body.text = etree.CDATA("")
+
+ # THIS FIXES FORMATTING
+ etree.indent(root, space=" ")
+
+ tree.write(file_path, encoding="utf-8", xml_declaration=True)
+
def check_website_sale_installed(self):
# Prepare the JSON-RPC payload to search for the 'website_sale' module
@@ -1427,6 +1620,88 @@ def add_require_depends(self, depends_list):
new_depends = ['knowledge']
depends_list = sorted(set(depends_list + new_depends))
return depends_list
+
+ def clean_homepage_view(self, destination_module_path):
+ path_ir_ui_view = Path(destination_module_path + '/demo/ir_ui_view.xml')
+
+ if not path_ir_ui_view.exists():
+ return
+
+ root = self.get_etree_content(path_ir_ui_view)
+ old_id = None
+
+ # Find homepage and update record id
+ records = root.xpath("//record")
+ for record in records:
+ t_nodes = record.xpath(".//t[@t-name='website.homepage']")
+ if t_nodes:
+ old_id = record.get("id")
+ record.set("id", "homepage")
+ break
+
+ if old_id is None:
+ return
+
+ # Update function reference
+ functions = root.xpath("//function")
+ extracted_functions = []
+
+ for func in functions:
+ values = func.xpath(".//value")
+
+ for val in values:
+ eval_attr = val.get("eval")
+
+ if eval_attr and f".{old_id}" in eval_attr:
+ new_eval = eval_attr.replace(f".{old_id}", ".homepage")
+ val.set("eval", new_eval)
+
+ extracted_functions.append(func)
+
+ # Update website_page.xml reference
+ path_website_page = Path(destination_module_path + '/demo/website_page.xml')
+
+ if path_website_page.exists():
+ page_root = self.get_etree_content(path_website_page)
+
+ records = page_root.xpath("//record")
+
+ for record in records:
+ view_fields = record.xpath("./field[@name='view_id']")
+
+ ref_attr = view_fields[0].get("ref") if view_fields else None
+
+ # check if ref matches old_id
+ if ref_attr and f"{old_id}" in ref_attr:
+ view_fields[0].set("ref", "homepage")
+
+ # save updated file
+ self.write_etree_content(path_website_page, page_root)
+
+ # Remove function from original file
+ for func in extracted_functions:
+ parent = func.getparent()
+ if parent is not None:
+ parent.remove(func)
+
+ # Save cleaned ir_ui_view.xml
+ self.write_etree_content(path_ir_ui_view, root)
+
+ # Add function to website_theme_apply.xml
+ if extracted_functions:
+ theme_apply_path = Path(destination_module_path + '/demo/website_theme_apply.xml')
+
+ if theme_apply_path.exists():
+ theme_root = self.get_etree_content(theme_apply_path)
+ else:
+ theme_root = etree.Element("odoo")
+
+ for func in extracted_functions:
+ theme_root.append(func)
+
+ self.write_etree_content(theme_apply_path, theme_root)
+
+ return
def arrange_manifest_file(self, destination_module_path, manifest_demo_file_list):
"""
@@ -1517,14 +1792,17 @@ def arrange_manifest_file(self, destination_module_path, manifest_demo_file_list
return
- def add_theme_immediate_install_function(self, destination_module_path):
+ def add_button_choose_theme_function(self, destination_module_path):
"""
- Adds an immediate install function for the theme module in the website_theme_apply.xml file.
+ Adds a theme selection function (button_choose_theme) to website_theme_apply.xml.
- This function reads the theme_id reference from the demo/website.xml file of the given module.
- It then generates a XML element to trigger the immediate installation of the theme module.
- The generated function block is appended inside the tag of demo/website_theme_apply.xml.
- If the target XML file does not exist, it creates a new one with the required structure.
+ This function extracts the theme_id from demo/website.xml of the given module.
+ It then generates a XML element that triggers theme selection using
+ the `button_choose_theme` method on `ir.module.module`.
+
+ The generated function block is added inside the tag of
+ demo/website_theme_apply.xml. If the file does not exist, it creates
+ a new XML file with the required structure.
Args:
destination_module_path (str): The module directory name containing the demo folder with website.xml.
@@ -1532,11 +1810,21 @@ def add_theme_immediate_install_function(self, destination_module_path):
website_path = Path(destination_module_path + '/demo/' + 'website.xml')
if website_path.exists():
etree_content = self.get_etree_content(website_path)
- theme_id = etree_content.xpath("//field[@name='theme_id']")[0].get('ref')
+ theme_id = etree_content.xpath("//field[@name='theme_id']")
if theme_id:
# Build new entries for each SCSS customization
- new_function = f""""""
+ field_node = theme_id[0]
+ old_ref = field_node.get('ref')
+ new_function = f""""""
+
+ field_node.attrib.pop('ref', None)
+ field_node.set(
+ 'search',
+ f"[('id', '=', ref('{old_ref}', raise_if_not_found=False))]"
+ )
+
+ self.write_etree_content(website_path, etree_content)
# Base structure if file does not exist
base_xml = f"""
@@ -1643,13 +1931,30 @@ def clean_hr_employee(self, destination_module_path):
# remove the records only which starts with base_industry_data.
def clean_res_partner(self, destination_module_path):
"""
- -remove the records only which starts with base_industry_data.
+ -remove the records only which starts with base_industry_data and transform emails to lowercase.
"""
path_res_partner = Path(destination_module_path + '/demo/' + 'res_partner.xml')
if path_res_partner.exists():
root_res_partner = self.get_etree_content(path_res_partner)
records = root_res_partner.xpath("//record")
for record in records:
+ # find email field inside record
+ email_field = record.xpath("./field[@name='email']")
+
+ if email_field:
+ email = email_field[0].text
+
+ if email and '@' in email:
+
+ # split before and after @
+ local_part, domain = email.split('@', 1)
+
+ # make only before @ lowercase
+ local_part = local_part.lower()
+
+ # update email
+ email_field[0].text = f"{local_part}@{domain}"
+
record_id = record.get('id', '')
if record_id.startswith("base_industry_data."):
root_res_partner.remove(record)
diff --git a/automation_script/only_cleanup_script.py b/automation_script/only_cleanup_script.py
index a6c6d2feee..6bc40ed450 100644
--- a/automation_script/only_cleanup_script.py
+++ b/automation_script/only_cleanup_script.py
@@ -16,11 +16,13 @@
import shutil
import argparse
import sys
+import psycopg2
from pathlib import Path
import re
from ast import literal_eval
from lxml import etree
+from lxml import html
# Setup logger
# Create a Logger instance directly
@@ -64,12 +66,12 @@ def __init__(self, ind_name, ind_category, db_name, module_path, destination_bas
'demo/website_page.xml',
'demo/account_analytic_plan.xml',
'demo/crm_team.xml',
- 'data/uom.uom.xml',
+ 'data/uom_uom.xml',
]
self.mandatory_files = {
"/data/mail_message.xml": """
-
+
discuss.channel
email
@@ -78,7 +80,7 @@ def __init__(self, ind_name, ind_category, db_name, module_path, destination_bas
🚀 Get started with Odoo {Ind_name}
@@ -221,7 +223,6 @@ def clean(self):
'sale_async_emails',
'snailmail_account',
'web_grid',
- 'web_studio',
'social_push_notifications',
'appointment_sms',
'website_knowledge',
@@ -258,6 +259,9 @@ def clean(self):
elif current_dir.endswith('/ir_attachment/') and ext == "scss":
self.get_relevant_scss_data(scss_content_list, root, file_name)
+ if not scss_content_list:
+ self.get_relevant_scss_data_from_db(scss_content_list)
+
# Generate SCSS function from collected theme data
self.write_scss_function(destination_module_path, scss_content_list)
@@ -278,11 +282,14 @@ def clean(self):
# Retain only welcome article in the knowledge article
self.clean_knowledge_article(destination_module_path)
+ # Transform knowledge article by extracting HTML content into a QWeb template
+ self.transform_knowledge_article(destination_module_path)
+
# Add demo payment provider if relevant module is present
self.add_demo_payment_provider(destination_module_path, manifest_demo_file_list)
- # Add immediate install function for the theme module in demo XML files
- self.add_theme_immediate_install_function(destination_module_path)
+ # Add theme selection function using button_choose_theme in demo XML
+ self.add_button_choose_theme_function(destination_module_path)
# Clean up sale order line records
self.clean_sale_order_line_record(destination_module_path)
@@ -299,6 +306,9 @@ def clean(self):
# Clean up pos.payment.method records
self.clean_pos_payment_method(destination_module_path)
+ # Clean and edit homepage view(ir_ui_view)
+ self.clean_homepage_view(destination_module_path)
+
# Update demo file order in manifest
self.arrange_manifest_file(destination_module_path, manifest_demo_file_list)
@@ -642,6 +652,70 @@ def remove_computed_fields(self, fields_info_dict, model_name, record, content):
content = pattern_self_closing.sub('', content)
return content
+
+ def get_relevant_scss_data_from_db(self, scss_content_list):
+ conn = psycopg2.connect(
+ dbname=self.db_name,
+ )
+
+ cr = conn.cursor()
+
+ query = """
+ SELECT url, index_content
+ FROM ir_attachment
+ WHERE name IN (
+ 'user_color_palette.scss',
+ 'user_values.scss',
+ 'user_theme_color_palette.scss'
+ )
+ """
+
+ cr.execute(query)
+
+ records = cr.fetchall()
+
+ scss_pattern = re.compile(r'o-map-omit\(\(\s*(.*?)\s*(?://\s*--\s*hook\s*--)?\s*\)\)', re.DOTALL)
+ for file_name, raw in records:
+ clean_url = "/website/" + file_name.split('website/')[1]
+
+ scss_content_dict = {}
+
+ scss_content = raw.decode() if isinstance(raw, bytes) else raw
+
+ scss_match = scss_pattern.search(scss_content)
+
+ if scss_match:
+ lines = []
+
+ for line in scss_match.group(1).splitlines():
+ if not line:
+ continue
+
+ line = line.strip()
+
+ if ":" in line:
+ key, value = line.split(":", 1)
+
+ value = value.strip().rstrip(",")
+ if value == "NULL":
+ value = "null"
+ if not (value.startswith("'") and value.endswith("'")):
+ value = f"'{value}'"
+
+ line = f"{key}: {value},"
+
+ lines.append(line)
+
+ inner_scss_content = "".join(lines)
+
+ scss_content_dict['url'] = clean_url
+ scss_content_dict['inner_scss_content'] = inner_scss_content
+
+ scss_content_list.append(scss_content_dict)
+
+
+ cr.close()
+ conn.close()
def get_relevant_scss_data(self, scss_content_list, root, file_name):
"""
@@ -854,6 +928,10 @@ def remove_unused_ir_attachment_post(self, destination_module_path):
record.remove(res_model[0])
if website_id:
record.remove(website_id[0])
+ existing = record.xpath("./field[@name='public']")
+ if not existing:
+ new_field = etree.Element("field", name="public", eval="True")
+ record.append(new_field)
if key_field or name_field:
# check key or name in ir_ui_view.xml file if not found store in list
key = key_field[0].text if key_field else None
@@ -1015,6 +1093,10 @@ def clean_knowledge_article(self, destination_module_path):
if not record.xpath('.//field[@name="is_locked"]'):
new_field = etree.Element("field", name="is_locked", eval="True")
record.append(new_field)
+
+ # Remove 'category' field if exists
+ if category_fields := record.xpath('.//field[@name="category"]'):
+ record.remove(category_fields[0])
else:
# Remove all other records
@@ -1023,6 +1105,47 @@ def clean_knowledge_article(self, destination_module_path):
self.write_etree_content(path_knowledge_article, root_knowledge_article)
return
+ def transform_knowledge_article(self, destination_module_path):
+ file_path = Path(destination_module_path + '/data/knowledge_article.xml')
+
+ if not file_path.exists():
+ return
+
+ parser = etree.XMLParser(remove_blank_text=True)
+ tree = etree.parse(file_path, parser)
+ root = tree.getroot()
+
+ body_fields = root.xpath("//record[@id='welcome_article']/field[@name='body']")
+ if body_fields is None:
+ return
+ cnt = 0
+ for body in body_fields:
+
+ if body.text:
+ extracted_content = body.text
+ body.text = ""
+ template = etree.Element("template", id="welcome_article_body")
+ wrapper = html.fragment_fromstring(extracted_content, create_parent=True)
+
+ # Remove attribute data-heading-link-id
+ for el in wrapper.xpath('//*[@data-heading-link-id]'):
+ del el.attrib['data-heading-link-id']
+
+ # Append all children
+ template.extend(wrapper)
+
+ root.insert(cnt, template)
+ cnt+=2
+ else:
+ continue
+
+ body.text = etree.CDATA("")
+
+ # THIS FIXES FORMATTING
+ etree.indent(root, space=" ")
+
+ tree.write(file_path, encoding="utf-8", xml_declaration=True)
+
def check_website_sale_installed(self):
# Prepare the JSON-RPC payload to search for the 'website_sale' module
@@ -1099,6 +1222,88 @@ def add_require_depends(self, depends_list):
depends_list = sorted(set(depends_list + new_depends))
return depends_list
+ def clean_homepage_view(self, destination_module_path):
+ path_ir_ui_view = Path(destination_module_path + '/demo/ir_ui_view.xml')
+
+ if not path_ir_ui_view.exists():
+ return
+
+ root = self.get_etree_content(path_ir_ui_view)
+ old_id = None
+
+ # Find homepage and update record id
+ records = root.xpath("//record")
+ for record in records:
+ t_nodes = record.xpath(".//t[@t-name='website.homepage']")
+ if t_nodes:
+ old_id = record.get("id")
+ record.set("id", "homepage")
+ break
+
+ if old_id is None:
+ return
+
+ # Update function reference
+ functions = root.xpath("//function")
+ extracted_functions = []
+
+ for func in functions:
+ values = func.xpath(".//value")
+
+ for val in values:
+ eval_attr = val.get("eval")
+
+ if eval_attr and f".{old_id}" in eval_attr:
+ new_eval = eval_attr.replace(f".{old_id}", ".homepage")
+ val.set("eval", new_eval)
+
+ extracted_functions.append(func)
+
+ # Update website_page.xml reference
+ path_website_page = Path(destination_module_path + '/demo/website_page.xml')
+
+ if path_website_page.exists():
+ page_root = self.get_etree_content(path_website_page)
+
+ records = page_root.xpath("//record")
+
+ for record in records:
+ view_fields = record.xpath("./field[@name='view_id']")
+
+ ref_attr = view_fields[0].get("ref") if view_fields else None
+
+ # check if ref matches old_id
+ if ref_attr and f"{old_id}" in ref_attr:
+ view_fields[0].set("ref", "homepage")
+
+ # save updated file
+ self.write_etree_content(path_website_page, page_root)
+
+ # Remove function from original file
+ for func in extracted_functions:
+ parent = func.getparent()
+ if parent is not None:
+ parent.remove(func)
+
+ # Save cleaned ir_ui_view.xml
+ self.write_etree_content(path_ir_ui_view, root)
+
+ # Add function to website_theme_apply.xml
+ if extracted_functions:
+ theme_apply_path = Path(destination_module_path + '/demo/website_theme_apply.xml')
+
+ if theme_apply_path.exists():
+ theme_root = self.get_etree_content(theme_apply_path)
+ else:
+ theme_root = etree.Element("odoo")
+
+ for func in extracted_functions:
+ theme_root.append(func)
+
+ self.write_etree_content(theme_apply_path, theme_root)
+
+ return
+
def arrange_manifest_file(self, destination_module_path, manifest_demo_file_list):
"""
Finalizes the demo file arrangement and updates the __manifest__.py accordingly.
@@ -1188,14 +1393,17 @@ def arrange_manifest_file(self, destination_module_path, manifest_demo_file_list
return
- def add_theme_immediate_install_function(self, destination_module_path):
+ def add_button_choose_theme_function(self, destination_module_path):
"""
- Adds an immediate install function for the theme module in the website_theme_apply.xml file.
+ Adds a theme selection function (button_choose_theme) to website_theme_apply.xml.
+
+ This function extracts the theme_id from demo/website.xml of the given module.
+ It then generates a XML element that triggers theme selection using
+ the `button_choose_theme` method on `ir.module.module`.
- This function reads the theme_id reference from the demo/website.xml file of the given module.
- It then generates a XML element to trigger the immediate installation of the theme module.
- The generated function block is appended inside the tag of demo/website_theme_apply.xml.
- If the target XML file does not exist, it creates a new one with the required structure.
+ The generated function block is added inside the tag of
+ demo/website_theme_apply.xml. If the file does not exist, it creates
+ a new XML file with the required structure.
Args:
destination_module_path (str): The module directory name containing the demo folder with website.xml.
@@ -1203,11 +1411,21 @@ def add_theme_immediate_install_function(self, destination_module_path):
website_path = Path(destination_module_path + '/demo/' + 'website.xml')
if website_path.exists():
etree_content = self.get_etree_content(website_path)
- theme_id = etree_content.xpath("//field[@name='theme_id']")[0].get('ref')
+ theme_id = etree_content.xpath("//field[@name='theme_id']")
if theme_id:
# Build new entries for each SCSS customization
- new_function = f""""""
+ field_node = theme_id[0]
+ old_ref = field_node.get('ref')
+ new_function = f""""""
+
+ field_node.attrib.pop('ref', None)
+ field_node.set(
+ 'search',
+ f"[('id', '=', ref('{old_ref}', raise_if_not_found=False))]"
+ )
+
+ self.write_etree_content(website_path, etree_content)
# Base structure if file does not exist
base_xml = f"""
@@ -1313,13 +1531,30 @@ def clean_hr_employee(self, destination_module_path):
# remove the records only which starts with base_industry_data.
def clean_res_partner(self, destination_module_path):
"""
- -remove the records only which starts with base_industry_data.
+ -remove the records only which starts with base_industry_data and transform emails to lowercase.
"""
path_res_partner = Path(destination_module_path + '/demo/' + 'res_partner.xml')
if path_res_partner.exists():
root_res_partner = self.get_etree_content(path_res_partner)
records = root_res_partner.xpath("//record")
for record in records:
+ # find email field inside record
+ email_field = record.xpath("./field[@name='email']")
+
+ if email_field:
+ email = email_field[0].text
+
+ if email and '@' in email:
+
+ # split before and after @
+ local_part, domain = email.split('@', 1)
+
+ # make only before @ lowercase
+ local_part = local_part.lower()
+
+ # update email
+ email_field[0].text = f"{local_part}@{domain}"
+
record_id = record.get('id', '')
if record_id.startswith("base_industry_data."):
root_res_partner.remove(record)