[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:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: odoo
|
POSTGRES_DB: odoo
|
||||||
POSTGRES_PASSWORD: odoo
|
POSTGRES_PASSWORD: odoo
|
||||||
POSTGRES_USER: odoo
|
POSTGRES_USER: odoo
|
||||||
ports:
|
# ports:
|
||||||
- "5432:5432"
|
# - "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data_addons_cm:/var/lib/postgresql/data
|
- postgres_data_addons_cm:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U odoo"]
|
test: ["CMD-SHELL", "pg_isready -U odoo"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
odoo:
|
odoo:
|
||||||
image: odoo:18
|
image: odoo:18
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "8069:8069"
|
- "8070:8069"
|
||||||
- "8072:8072"
|
- "8073:8072"
|
||||||
environment:
|
environment:
|
||||||
HOST: 0.0.0.0
|
HOST: 0.0.0.0
|
||||||
PORT: "8069"
|
PORT: "8069"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/mnt/extra-addons/
|
- ./:/mnt/extra-addons/
|
||||||
- ./odoo.conf:/etc/odoo/odoo.conf:ro
|
- ./odoo.conf:/etc/odoo/odoo.conf:ro
|
||||||
- odoo_data_addons_cm:/var/lib/odoo
|
- odoo_data_addons_cm:/var/lib/odoo
|
||||||
command: odoo -c /etc/odoo/odoo.conf
|
command: odoo -c /etc/odoo/odoo.conf
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data_addons_cm:
|
postgres_data_addons_cm:
|
||||||
odoo_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/),
|
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).
|
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
|
## [18.0.2.4.0] - 2026-02-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
{ # noqa: B018
|
{ # noqa: B018
|
||||||
"name": "Product Sale Price from Pricelist",
|
"name": "Product Sale Price from Pricelist",
|
||||||
"version": "18.0.2.4.0",
|
"version": "18.0.2.5.0",
|
||||||
"category": "product",
|
"category": "product",
|
||||||
"summary": "Set sale price from pricelist based on last purchase price",
|
"summary": "Set sale price from pricelist based on last purchase price",
|
||||||
"author": "Odoo Community Association (OCA), Criptomart",
|
"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