[FIX] product_sale_price_from_pricelist: Convert JSONB columns in product_template too

This commit is contained in:
snt 2026-02-14 18:15:15 +01:00
parent 3eae4fa884
commit b5410d24bc
5 changed files with 268 additions and 108 deletions

View file

@ -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:

View file

@ -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

View file

@ -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",

View file

@ -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,
)

View file

@ -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")