Skip to content

Commit b57b9ce

Browse files
bosdbosd
authored andcommitted
[FIX]: Product Template Attribute Mapping V13+
1 parent a36ade5 commit b57b9ce

File tree

2 files changed

+137
-52
lines changed

2 files changed

+137
-52
lines changed

src/odoo_data_flow/lib/mapper.py

Lines changed: 66 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@
5858

5959
def _get_field_value(line: LineDict, field: str, default: Any = "") -> Any:
6060
"""Safely retrieves a value from the current data row."""
61-
return line.get(field, default) or default
61+
value = line.get(field, default)
62+
log.debug(
63+
f"Getting field '{field}': value='{value}' from line keys: {list(line.keys())}"
64+
)
65+
return value
6266

6367

6468
def _str_to_mapper(field: Any) -> MapperFunc:
@@ -401,74 +405,88 @@ def m2o_att_fun(line: LineDict, state: StateDict) -> dict[str, str]:
401405

402406

403407
def m2m_id_list(
404-
prefix: str, *fields: Any, sep: str = ",", const_values: Optional[list[str]] = None
408+
prefix: str, *args: Any, sep: str = ",", const_values: Optional[list[str]] = None
405409
) -> ListMapperFunc:
406410
"""Returns a mapper for creating a list of M2M external IDs.
407411
408-
This is primarily used when creating the related records for a M2M field,
409-
such as creating all unique `res.partner.category` records.
410-
411-
Args:
412-
prefix: The XML ID prefix to apply to each value.
413-
*fields: One or more source fields to read values from.
414-
sep: The separator to use when splitting values.
415-
const_values: A list of constant XML IDs to always include.
416-
417-
Returns:
418-
A mapper function that returns a list of individual external IDs.
412+
This function can take either raw field names (str) or other mapper functions
413+
as its arguments. It processes each argument to produce an individual ID.
414+
If a field's value contains the separator, it will be split.
419415
"""
420416
if const_values is None:
421417
const_values = []
422418

423419
def m2m_id_list_fun(line: LineDict, state: StateDict) -> list[str]:
424-
# Call a shared internal helper to get unique, ordered raw values
425-
raw_values = _get_unique_raw_values_from_fields(
426-
line, state, fields, sep, const_values
427-
)
428-
# Apply prefixing to each raw value
429-
return [to_m2o(prefix, v) for v in raw_values if v]
420+
all_ids: list[str] = []
421+
for arg in args:
422+
# Determine if arg is a field name or an already-created mapper
423+
if isinstance(arg, str):
424+
raw_value = _get_field_value(line, arg)
425+
elif callable(arg): # Assume it's a mapper function
426+
try:
427+
raw_value = arg(line, state)
428+
except (
429+
TypeError
430+
): # Fallback for mappers not taking 'state' (less common now)
431+
raw_value = arg(line)
432+
else:
433+
raw_value = "" # Or raise error, depending on desired strictness
434+
435+
if raw_value and isinstance(raw_value, str):
436+
# Always split values by separator if they contain it.
437+
# This ensures "Color_Black" and "Gender_Woman" are separate.
438+
parts = [v.strip() for v in raw_value.split(sep) if v.strip()]
439+
all_ids.extend([to_m2o(prefix, p) for p in parts])
440+
elif raw_value: # If not string but truthy (e.g., a number from mapper.num)
441+
all_ids.append(to_m2o(prefix, str(raw_value)))
442+
443+
# Add constant values, applying prefix
444+
all_ids.extend([to_m2o(prefix, cv) for cv in const_values if cv])
445+
446+
# Ensure uniqueness and preserve order
447+
unique_ids = list(dict.fromkeys(all_ids))
448+
return unique_ids
430449

431450
return m2m_id_list_fun
432451

433452

434453
def m2m_value_list(
435-
*fields: Any, sep: str = ",", const_values: Optional[list[str]] = None
436-
) -> ListMapperFunc: # Changed to ListMapperFunc
437-
"""Returns a mapper that creates a Python list of unique values."""
454+
*args: Any, sep: str = ",", const_values: Optional[list[str]] = None
455+
) -> ListMapperFunc:
456+
"""Returns a mapper that creates a Python list of unique raw values.
457+
458+
It processes each argument to produce an individual raw value.
459+
If a field's value contains the separator, it will be split.
460+
"""
438461
if const_values is None:
439462
const_values = []
440463

441464
def m2m_value_list_fun(line: LineDict, state: StateDict) -> list[str]:
442-
# Call the same shared internal helper to get unique, ordered raw values
443-
raw_values = _get_unique_raw_values_from_fields(
444-
line, state, fields, sep, const_values
445-
)
446-
# Return the raw values directly
447-
return [v for v in raw_values if v]
448-
449-
return m2m_value_list_fun
450-
465+
"""Returns a mapper that creates a Python list of unique values."""
466+
all_values: list[str] = []
467+
for arg in args:
468+
if isinstance(arg, str):
469+
raw_value = _get_field_value(line, arg)
470+
elif callable(arg):
471+
try:
472+
raw_value = arg(line, state)
473+
except TypeError:
474+
raw_value = arg(line)
475+
else:
476+
raw_value = ""
451477

452-
def _get_unique_raw_values_from_fields(
453-
line: LineDict, state: StateDict, fields: Any, sep: str, const_values: list[str]
454-
) -> list[str]:
455-
"""Helper to get unique, ordered raw values from fields and const_values."""
456-
all_raw_values: list[str] = []
457-
concat_m = concat("", *fields)
478+
if raw_value and isinstance(raw_value, str):
479+
parts = [v.strip() for v in raw_value.split(sep) if v.strip()]
480+
all_values.extend(parts)
481+
elif raw_value: # If not string but truthy
482+
all_values.append(str(raw_value))
458483

459-
value_from_fields = concat_m(line, state)
460-
if value_from_fields:
461-
# Split and extend with values from fields
462-
all_raw_values.extend(
463-
[v.strip() for v in value_from_fields.split(sep) if v.strip()]
464-
)
484+
all_values.extend([v.strip() for v in const_values if v.strip()])
465485

466-
# Extend with constant values
467-
all_raw_values.extend([v.strip() for v in const_values if v.strip()])
486+
unique_values = list(dict.fromkeys(all_values))
487+
return unique_values
468488

469-
# Preserve order and ensure uniqueness using dict.fromkeys (Python 3.7+)
470-
unique_ordered_values = list(dict.fromkeys(all_raw_values))
471-
return unique_ordered_values
489+
return m2m_value_list_fun
472490

473491

474492
def map_val(

src/odoo_data_flow/lib/transform.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
import csv
44
import os
55
from collections import OrderedDict
6-
from typing import Any, Callable, Optional, Union
6+
from typing import (
7+
Any,
8+
Callable,
9+
Optional,
10+
Union,
11+
)
712

813
from lxml import etree # type: ignore[import-untyped]
914

@@ -70,7 +75,6 @@ def __init__(
7075
self.header: list[str]
7176
self.data: list[list[Any]]
7277

73-
# Determine if initializing from a file or in-memory data
7478
if filename:
7579
self.header, self.data = self._read_file(
7680
filename, separator, encoding, **kwargs
@@ -84,7 +88,6 @@ def __init__(
8488
" 'header' and 'data'."
8589
)
8690

87-
# Apply any pre-processing hooks
8891
self.header, self.data = preprocess(self.header, self.data)
8992

9093
def _read_file(
@@ -140,7 +143,7 @@ def _read_file(
140143
return [], []
141144
except Exception as e:
142145
log.error(f"Failed to read file {filename}: {e}")
143-
return [], []
146+
return [], []
144147

145148
def check(
146149
self, check_fun: Callable[..., bool], message: Optional[str] = None
@@ -429,6 +432,70 @@ def process_attribute_data(
429432
]
430433
self._add_data(attr_header, attr_data, filename_out, import_args)
431434

435+
# NEW METHOD for product.attribute.value.csv
436+
def process_attribute_value_data(
437+
self,
438+
attribute_list: list[str],
439+
attribute_value_prefix: str,
440+
attribute_prefix: str,
441+
filename_out: str,
442+
import_args: dict[str, Any],
443+
) -> None:
444+
"""Collects unique attribute values.
445+
446+
Collects unique attribute values from all product lines and prepares them
447+
for product.attribute.value.csv.
448+
449+
Args:
450+
attribute_list: List of attribute column names (e.g., ['Color', 'Size_H']).
451+
attribute_value_prefix: The XML ID prefix for attribute values.
452+
attribute_prefix: The XML ID prefix for attributes themselves.
453+
filename_out: The output path for product.attribute.value.csv.
454+
import_args: Import parameters for the script.
455+
"""
456+
unique_attribute_values: set[tuple[str, str, str]] = set()
457+
458+
# Iterate over all raw data lines
459+
for raw_line in self.data:
460+
line_dict = dict(
461+
zip(self.header, raw_line)
462+
) # Convert to dict for easy access
463+
464+
for attribute_field in attribute_list:
465+
# Get the raw value for this specific attribute
466+
# (e.g., "Black" for "Color")
467+
value_raw = line_dict.get(attribute_field, "").strip()
468+
469+
if value_raw:
470+
# Form the ID for the attribute value
471+
# (e.g., PRODUCT_ATTRIBUTE_VALUE.Color_Black)
472+
# Using concat_field_value_m2m's logic implicitly:
473+
# AttributeName_Value
474+
attr_value_id = mapper.to_m2o(
475+
attribute_value_prefix, f"{attribute_field}_{value_raw}"
476+
)
477+
# The name is just the raw value (e.g., "Black")
478+
attr_value_name = value_raw
479+
# The attribute_id/id is just the attribute name prefixed
480+
# (e.g., PRODUCT_ATTRIBUTE.Color)
481+
attr_id = mapper.to_m2o(attribute_prefix, attribute_field)
482+
483+
unique_attribute_values.add(
484+
(attr_value_id, attr_value_name, attr_id)
485+
)
486+
487+
# Convert the set of tuples to a list of lists for writing
488+
attr_values_data: list[list[str]] = [
489+
list(item) for item in unique_attribute_values
490+
]
491+
attr_values_header: list[str] = [
492+
"id",
493+
"name",
494+
"attribute_id/id",
495+
]
496+
497+
self._add_data(attr_values_header, attr_values_data, filename_out, import_args)
498+
432499

433500
class ProductProcessorV9(Processor):
434501
"""Processor for the legacy (Odoo v9-v12) product attribute model."""

0 commit comments

Comments
 (0)