feat: extract workflow material services phase 3
This commit is contained in:
@@ -26,6 +26,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
EmitFn = Callable[..., None] | None
|
||||
SetupStatus = Literal["ready", "skip", "failed", "missing"]
|
||||
QueueThumbnailFn = Callable[[str, dict[str, str]], None] | None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -68,6 +69,24 @@ class TemplateResolutionResult:
|
||||
output_type_id: str | None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MaterialResolutionResult:
|
||||
material_map: dict[str, str] | None
|
||||
use_materials: bool
|
||||
override_material: str | None
|
||||
source_material_count: int = 0
|
||||
resolved_material_count: int = 0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AutoPopulateMaterialsResult:
|
||||
cad_file_id: str
|
||||
updated_product_ids: list[str] = field(default_factory=list)
|
||||
queued_thumbnail_regeneration: bool = False
|
||||
part_colors: dict[str, str] | None = None
|
||||
cad_parts: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _emit(emit: EmitFn, order_line_id: str, message: str, level: str | None = None) -> None:
|
||||
if emit is None:
|
||||
return
|
||||
@@ -270,34 +289,14 @@ def resolve_order_line_template_context(
|
||||
output_type_id=output_type_id,
|
||||
)
|
||||
material_library = get_material_library_path_for_session(session)
|
||||
|
||||
material_map = None
|
||||
use_materials = bool(material_library and materials_source)
|
||||
if template and not template.material_replace_enabled:
|
||||
use_materials = False
|
||||
if use_materials:
|
||||
material_map = {
|
||||
material["part_name"]: material["material"]
|
||||
for material in materials_source
|
||||
if material.get("part_name") and material.get("material")
|
||||
}
|
||||
material_map = resolve_material_map(material_map)
|
||||
|
||||
line_override = getattr(line, "material_override", None)
|
||||
output_override = line.output_type.material_override if line.output_type else None
|
||||
override_material = line_override or output_override
|
||||
if override_material:
|
||||
override_keys = set(material_map.keys()) if material_map else set()
|
||||
if cad_file and cad_file.parsed_objects:
|
||||
for part_name in cad_file.parsed_objects.get("objects", []):
|
||||
override_keys.add(part_name)
|
||||
material_map = {key: override_material for key in override_keys}
|
||||
use_materials = True
|
||||
_emit(
|
||||
emit,
|
||||
str(line.id),
|
||||
f"Material override active: {len(material_map)} parts → {override_material}",
|
||||
)
|
||||
material_resolution = resolve_order_line_material_map(
|
||||
line,
|
||||
cad_file,
|
||||
materials_source,
|
||||
material_library=material_library,
|
||||
template=template,
|
||||
emit=emit,
|
||||
)
|
||||
|
||||
if template:
|
||||
_emit(
|
||||
@@ -325,11 +324,151 @@ def resolve_order_line_template_context(
|
||||
return TemplateResolutionResult(
|
||||
template=template,
|
||||
material_library=material_library,
|
||||
material_map=material_resolution.material_map,
|
||||
use_materials=material_resolution.use_materials,
|
||||
override_material=material_resolution.override_material,
|
||||
category_key=category_key,
|
||||
output_type_id=output_type_id,
|
||||
)
|
||||
|
||||
|
||||
def resolve_order_line_material_map(
|
||||
line: OrderLine,
|
||||
cad_file: CadFile | None,
|
||||
materials_source: list[dict[str, Any]],
|
||||
*,
|
||||
material_library: str | None,
|
||||
template: RenderTemplate | None,
|
||||
emit: EmitFn = None,
|
||||
) -> MaterialResolutionResult:
|
||||
"""Resolve the effective order-line material map with legacy precedence rules."""
|
||||
material_map = None
|
||||
raw_material_count = 0
|
||||
use_materials = bool(material_library and materials_source)
|
||||
if template and not template.material_replace_enabled:
|
||||
use_materials = False
|
||||
if use_materials:
|
||||
material_map = {
|
||||
material["part_name"]: material["material"]
|
||||
for material in materials_source
|
||||
if material.get("part_name") and material.get("material")
|
||||
}
|
||||
raw_material_count = len(material_map)
|
||||
material_map = resolve_material_map(material_map)
|
||||
|
||||
line_override = getattr(line, "material_override", None)
|
||||
output_override = line.output_type.material_override if line.output_type else None
|
||||
override_material = line_override or output_override
|
||||
if override_material:
|
||||
override_keys = set(material_map.keys()) if material_map else set()
|
||||
if cad_file and cad_file.parsed_objects:
|
||||
for part_name in cad_file.parsed_objects.get("objects", []):
|
||||
override_keys.add(part_name)
|
||||
material_map = {key: override_material for key in override_keys}
|
||||
use_materials = True
|
||||
_emit(
|
||||
emit,
|
||||
str(line.id),
|
||||
f"Material override active: {len(material_map)} parts → {override_material}",
|
||||
)
|
||||
|
||||
return MaterialResolutionResult(
|
||||
material_map=material_map,
|
||||
use_materials=use_materials,
|
||||
override_material=override_material,
|
||||
category_key=category_key,
|
||||
output_type_id=output_type_id,
|
||||
source_material_count=raw_material_count,
|
||||
resolved_material_count=len(material_map or {}),
|
||||
)
|
||||
|
||||
|
||||
def auto_populate_materials_for_cad(
|
||||
session: Session,
|
||||
cad_file_id: str,
|
||||
*,
|
||||
enqueue_thumbnail: QueueThumbnailFn = None,
|
||||
) -> AutoPopulateMaterialsResult:
|
||||
"""Auto-fill empty CAD material mappings from Excel component data.
|
||||
|
||||
This preserves the legacy rules:
|
||||
- only products with empty/all-blank `cad_part_materials` are updated
|
||||
- thumbnail regeneration is queued at most once per CAD file
|
||||
- the queued part-color map comes from the last updated product
|
||||
"""
|
||||
from app.api.routers.products import build_materials_from_excel
|
||||
|
||||
cad_file = session.execute(
|
||||
select(CadFile).where(CadFile.id == cad_file_id)
|
||||
).scalar_one_or_none()
|
||||
if cad_file is None:
|
||||
return AutoPopulateMaterialsResult(cad_file_id=str(cad_file_id))
|
||||
|
||||
parsed_objects = cad_file.parsed_objects or {}
|
||||
cad_parts: list[str] = parsed_objects.get("objects", [])
|
||||
if not cad_parts:
|
||||
return AutoPopulateMaterialsResult(
|
||||
cad_file_id=str(cad_file_id),
|
||||
cad_parts=[],
|
||||
)
|
||||
|
||||
products = session.execute(
|
||||
select(Product).where(
|
||||
Product.cad_file_id == cad_file.id,
|
||||
Product.is_active.is_(True),
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
updated_product_ids: list[str] = []
|
||||
final_part_colors: dict[str, str] | None = None
|
||||
for product in products:
|
||||
excel_components: list[dict[str, Any]] = product.components or []
|
||||
if not excel_components:
|
||||
continue
|
||||
|
||||
existing = product.cad_part_materials or []
|
||||
if existing and any((entry.get("material") or "").strip() for entry in existing):
|
||||
continue
|
||||
|
||||
new_materials = build_materials_from_excel(cad_parts, excel_components)
|
||||
session.execute(
|
||||
sql_update(Product)
|
||||
.where(Product.id == product.id)
|
||||
.values(cad_part_materials=new_materials)
|
||||
)
|
||||
session.flush()
|
||||
updated_product_ids.append(str(product.id))
|
||||
|
||||
try:
|
||||
final_part_colors = build_part_colors(cad_parts, new_materials)
|
||||
except Exception:
|
||||
logger.exception("Part colors build failed for product %s", product.id)
|
||||
|
||||
logger.info(
|
||||
"Auto-populated %d materials for product %s from %d Excel components",
|
||||
len(new_materials),
|
||||
product.id,
|
||||
len(excel_components),
|
||||
)
|
||||
|
||||
session.commit()
|
||||
|
||||
queued_thumbnail_regeneration = False
|
||||
if final_part_colors is not None:
|
||||
if enqueue_thumbnail is None:
|
||||
from app.domains.pipeline.tasks.render_thumbnail import regenerate_thumbnail
|
||||
|
||||
enqueue_thumbnail = lambda current_cad_file_id, part_colors: regenerate_thumbnail.delay( # noqa: E731
|
||||
current_cad_file_id,
|
||||
part_colors,
|
||||
)
|
||||
enqueue_thumbnail(str(cad_file_id), final_part_colors)
|
||||
queued_thumbnail_regeneration = True
|
||||
|
||||
return AutoPopulateMaterialsResult(
|
||||
cad_file_id=str(cad_file_id),
|
||||
updated_product_ids=updated_product_ids,
|
||||
queued_thumbnail_regeneration=queued_thumbnail_regeneration,
|
||||
part_colors=final_part_colors,
|
||||
cad_parts=cad_parts,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user