[FIX] product_sale_price_from_pricelist: Convert JSONB columns in product_template too
This commit is contained in:
parent
3eae4fa884
commit
b5410d24bc
5 changed files with 268 additions and 108 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue