From b5410d24bce06ba990e14ebba119a3c7c7257a5f Mon Sep 17 00:00:00 2001 From: snt Date: Sat, 14 Feb 2026 18:15:15 +0100 Subject: [PATCH] [FIX] product_sale_price_from_pricelist: Convert JSONB columns in product_template too --- docker-compose.yml | 66 +++--- .../CHANGELOG.md | 11 + .../__manifest__.py | 2 +- .../migrations/18.0.2.4.0/post-migration.py | 74 ------ .../migrations/18.0.2.5.0/post-migration.py | 223 ++++++++++++++++++ 5 files changed, 268 insertions(+), 108 deletions(-) delete mode 100644 product_sale_price_from_pricelist/migrations/18.0.2.4.0/post-migration.py create mode 100644 product_sale_price_from_pricelist/migrations/18.0.2.5.0/post-migration.py diff --git a/docker-compose.yml b/docker-compose.yml index 0301896..711ac1b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,37 @@ services: - db: - image: postgres:15 - environment: - POSTGRES_DB: odoo - POSTGRES_PASSWORD: odoo - POSTGRES_USER: odoo - ports: - - "5432:5432" - volumes: - - postgres_data_addons_cm:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U odoo"] - interval: 5s - timeout: 5s - retries: 5 + db: + image: postgres:15 + environment: + POSTGRES_DB: odoo + POSTGRES_PASSWORD: odoo + POSTGRES_USER: odoo + # ports: + # - "5432:5432" + volumes: + - postgres_data_addons_cm:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U odoo"] + interval: 5s + timeout: 5s + retries: 5 - odoo: - image: odoo:18 - depends_on: - db: - condition: service_healthy - ports: - - "8069:8069" - - "8072:8072" - environment: - HOST: 0.0.0.0 - PORT: "8069" - volumes: - - ./:/mnt/extra-addons/ - - ./odoo.conf:/etc/odoo/odoo.conf:ro - - odoo_data_addons_cm:/var/lib/odoo - command: odoo -c /etc/odoo/odoo.conf + odoo: + image: odoo:18 + depends_on: + db: + condition: service_healthy + ports: + - "8070:8069" + - "8073:8072" + environment: + HOST: 0.0.0.0 + PORT: "8069" + volumes: + - ./:/mnt/extra-addons/ + - ./odoo.conf:/etc/odoo/odoo.conf:ro + - odoo_data_addons_cm:/var/lib/odoo + command: odoo -c /etc/odoo/odoo.conf volumes: - postgres_data_addons_cm: - odoo_data_addons_cm: + postgres_data_addons_cm: + odoo_data_addons_cm: diff --git a/product_sale_price_from_pricelist/CHANGELOG.md b/product_sale_price_from_pricelist/CHANGELOG.md index ef38340..158dcb0 100644 --- a/product_sale_price_from_pricelist/CHANGELOG.md +++ b/product_sale_price_from_pricelist/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [18.0.2.5.0] - 2026-02-14 + +### Fixed + +- **Complete JSONB to proper types conversion for both tables** + - Error: `TypeError: float() argument must be a string or a real number, not 'dict'` in computed fields + - Extended migration to convert JSONB columns in BOTH product_product AND product_template tables + - Previous migration only covered product_product, but product_template also had JSONB columns + - Now handles conversion for all 4 fields in both tables + - Preserves all existing data during conversion + ## [18.0.2.4.0] - 2026-02-14 ### Fixed diff --git a/product_sale_price_from_pricelist/__manifest__.py b/product_sale_price_from_pricelist/__manifest__.py index 32023e7..a463767 100644 --- a/product_sale_price_from_pricelist/__manifest__.py +++ b/product_sale_price_from_pricelist/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { # noqa: B018 "name": "Product Sale Price from Pricelist", - "version": "18.0.2.4.0", + "version": "18.0.2.5.0", "category": "product", "summary": "Set sale price from pricelist based on last purchase price", "author": "Odoo Community Association (OCA), Criptomart", diff --git a/product_sale_price_from_pricelist/migrations/18.0.2.4.0/post-migration.py b/product_sale_price_from_pricelist/migrations/18.0.2.4.0/post-migration.py deleted file mode 100644 index d2386b1..0000000 --- a/product_sale_price_from_pricelist/migrations/18.0.2.4.0/post-migration.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (C) 2026 Criptomart -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -import logging - -_logger = logging.getLogger(__name__) - - -def migrate(cr, version): - """Clean up old ir_property records for fields that are no longer company_dependent. - - This migration removes ir_property records that were created when these fields - were company_dependent. These records can cause TypeError: float() argument must - be a string or a real number, not 'dict' when Odoo tries to read the fields. - - Fields cleaned up: - - last_purchase_price_updated - - list_price_theoritical - - last_purchase_price_received - - last_purchase_price_compute_type - """ - _logger.info( - "Cleaning up ir_property records for product_sale_price_from_pricelist fields" - ) - - # Check if ir_property table exists - cr.execute(""" - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'ir_property' - ) - """) - table_exists = cr.fetchone()[0] - - if not table_exists: - _logger.info( - "Table ir_property does not exist yet, skipping cleanup (fresh install)" - ) - return - - # Delete all ir_property records for these fields on product.product - cr.execute(""" - DELETE FROM ir_property - WHERE name IN ( - 'last_purchase_price_updated', - 'list_price_theoritical', - 'last_purchase_price_received', - 'last_purchase_price_compute_type' - ) - AND res_id LIKE 'product.product,%%' - """) - deleted_count = cr.rowcount - _logger.info("Deleted %s ir_property records for product.product", deleted_count) - - # Also clean up for product.template if any exist - cr.execute(""" - DELETE FROM ir_property - WHERE name IN ( - 'last_purchase_price_updated', - 'list_price_theoritical', - 'last_purchase_price_received', - 'last_purchase_price_compute_type' - ) - AND res_id LIKE 'product.template,%%' - """) - deleted_template_count = cr.rowcount - _logger.info( - "Deleted %s ir_property records for product.template", deleted_template_count - ) - - _logger.info( - "Migration completed successfully. Total records deleted: %s", - deleted_count + deleted_template_count, - ) diff --git a/product_sale_price_from_pricelist/migrations/18.0.2.5.0/post-migration.py b/product_sale_price_from_pricelist/migrations/18.0.2.5.0/post-migration.py new file mode 100644 index 0000000..af80aac --- /dev/null +++ b/product_sale_price_from_pricelist/migrations/18.0.2.5.0/post-migration.py @@ -0,0 +1,223 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """Convert JSONB columns to proper types in product_product AND product_template. + + This migration handles the removal of company_dependent=True from fields. + When company_dependent is removed, Odoo may leave JSONB columns that need + explicit conversion to proper column types. + + Tables affected: + - product_product + - product_template + """ + if not version: + return + + _logger.info("Starting migration 18.0.2.5.0: JSONB → proper types conversion") + + # Convert product_product table + _convert_product_product_columns(cr) + + # Convert product_template table + _convert_product_template_columns(cr) + + _logger.info("Migration 18.0.2.5.0 completed successfully") + + +def _convert_product_product_columns(cr): + """Convert product_product JSONB columns to proper types.""" + _logger.info("Converting product_product JSONB columns...") + + # Check column types + cr.execute(""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'product_product' + AND column_name IN ( + 'last_purchase_price_updated', + 'list_price_theoritical', + 'last_purchase_price_received', + 'last_purchase_price_compute_type' + ) + """) + columns = {row[0]: row[1] for row in cr.fetchall()} + + # Convert each JSONB column if needed + if columns.get("last_purchase_price_updated") == "jsonb": + _logger.info( + "Converting product_product.last_purchase_price_updated from JSONB" + ) + _convert_jsonb_to_boolean(cr, "product_product", "last_purchase_price_updated") + + if columns.get("list_price_theoritical") == "jsonb": + _logger.info("Converting product_product.list_price_theoritical from JSONB") + _convert_jsonb_to_numeric(cr, "product_product", "list_price_theoritical") + + if columns.get("last_purchase_price_received") == "jsonb": + _logger.info( + "Converting product_product.last_purchase_price_received from JSONB" + ) + _convert_jsonb_to_numeric(cr, "product_product", "last_purchase_price_received") + + if columns.get("last_purchase_price_compute_type") == "jsonb": + _logger.info( + "Converting product_product.last_purchase_price_compute_type from JSONB" + ) + _convert_jsonb_to_varchar( + cr, "product_product", "last_purchase_price_compute_type" + ) + + +def _convert_product_template_columns(cr): + """Convert product_template JSONB columns to proper types.""" + _logger.info("Converting product_template JSONB columns...") + + # Check column types + cr.execute(""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'product_template' + AND column_name IN ( + 'last_purchase_price_updated', + 'list_price_theoritical', + 'last_purchase_price_received', + 'last_purchase_price_compute_type' + ) + """) + columns = {row[0]: row[1] for row in cr.fetchall()} + + # Convert each JSONB column if needed + if columns.get("last_purchase_price_updated") == "jsonb": + _logger.info( + "Converting product_template.last_purchase_price_updated from JSONB" + ) + _convert_jsonb_to_boolean(cr, "product_template", "last_purchase_price_updated") + + if columns.get("list_price_theoritical") == "jsonb": + _logger.info("Converting product_template.list_price_theoritical from JSONB") + _convert_jsonb_to_numeric(cr, "product_template", "list_price_theoritical") + + if columns.get("last_purchase_price_received") == "jsonb": + _logger.info( + "Converting product_template.last_purchase_price_received from JSONB" + ) + _convert_jsonb_to_numeric( + cr, "product_template", "last_purchase_price_received" + ) + + if columns.get("last_purchase_price_compute_type") == "jsonb": + _logger.info( + "Converting product_template.last_purchase_price_compute_type from JSONB" + ) + _convert_jsonb_to_varchar( + cr, "product_template", "last_purchase_price_compute_type" + ) + + +def _convert_jsonb_to_numeric( + cr, table_name, column_name +): # pylint: disable=sql-injection + """Convert a JSONB column to NUMERIC type extracting company value.""" + temp_col = f"{column_name}_new" + + # Create temp NUMERIC column + cr.execute(f"ALTER TABLE {table_name} ADD COLUMN {temp_col} NUMERIC") + + # Extract value from JSONB (company_id as key) + cr.execute(f""" + UPDATE {table_name} + SET {temp_col} = CAST( + ({column_name}->>(SELECT jsonb_object_keys({column_name}) LIMIT 1)) + AS NUMERIC + ) + WHERE {column_name} IS NOT NULL + AND {column_name}::text != 'null' + AND {column_name}::text LIKE '{{%' + """) + + # Drop old JSONB column + cr.execute(f"ALTER TABLE {table_name} DROP COLUMN {column_name} CASCADE") + + # Rename new column + cr.execute(f"ALTER TABLE {table_name} RENAME COLUMN {temp_col} TO {column_name}") + + # Set default for NULLs + cr.execute( + f"UPDATE {table_name} SET {column_name} = 0.0 WHERE {column_name} IS NULL" + ) + + _logger.info(f"✅ {table_name}.{column_name} converted from JSONB to NUMERIC") + + +def _convert_jsonb_to_boolean( + cr, table_name, column_name +): # pylint: disable=sql-injection + """Convert a JSONB column to BOOLEAN type extracting company value.""" + temp_col = f"{column_name}_new" + + # Create temp BOOLEAN column + cr.execute(f"ALTER TABLE {table_name} ADD COLUMN {temp_col} BOOLEAN") + + # Extract value from JSONB + cr.execute(f""" + UPDATE {table_name} + SET {temp_col} = CAST( + ({column_name}->>(SELECT jsonb_object_keys({column_name}) LIMIT 1)) + AS BOOLEAN + ) + WHERE {column_name} IS NOT NULL + AND {column_name}::text != 'null' + AND {column_name}::text LIKE '{{%' + """) + + # Drop old JSONB column + cr.execute(f"ALTER TABLE {table_name} DROP COLUMN {column_name} CASCADE") + + # Rename new column + cr.execute(f"ALTER TABLE {table_name} RENAME COLUMN {temp_col} TO {column_name}") + + # Set default for NULLs + cr.execute( + f"UPDATE {table_name} SET {column_name} = FALSE WHERE {column_name} IS NULL" + ) + + _logger.info(f"✅ {table_name}.{column_name} converted from JSONB to BOOLEAN") + + +def _convert_jsonb_to_varchar( + cr, table_name, column_name +): # pylint: disable=sql-injection + """Convert a JSONB column to VARCHAR type extracting company value.""" + temp_col = f"{column_name}_new" + + # Create temp VARCHAR column + cr.execute(f"ALTER TABLE {table_name} ADD COLUMN {temp_col} VARCHAR") + + # Extract value from JSONB + cr.execute(f""" + UPDATE {table_name} + SET {temp_col} = {column_name}->>(SELECT jsonb_object_keys({column_name}) LIMIT 1) + WHERE {column_name} IS NOT NULL + AND {column_name}::text != 'null' + AND {column_name}::text LIKE '{{%' + """) + + # Drop old JSONB column + cr.execute(f"ALTER TABLE {table_name} DROP COLUMN {column_name} CASCADE") + + # Rename new column + cr.execute(f"ALTER TABLE {table_name} RENAME COLUMN {temp_col} TO {column_name}") + + # Set default for NULLs + cr.execute( + f"UPDATE {table_name} SET {column_name} = 'without_discounts' WHERE {column_name} IS NULL" + ) + + _logger.info(f"✅ {table_name}.{column_name} converted from JSONB to VARCHAR")