209 lines
7.1 KiB
Python
209 lines
7.1 KiB
Python
import uuid
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
from typing import Dict, List, Optional
|
|
from datetime import datetime as dt
|
|
|
|
from pif_compiler.classes.models import (
|
|
StatoOrdine,
|
|
Ingredient,
|
|
Esposition,
|
|
)
|
|
|
|
from pif_compiler.functions.db_utils import (
|
|
db_connect,
|
|
upsert_cliente,
|
|
upsert_compilatore,
|
|
)
|
|
from pif_compiler.functions.common_log import get_logger
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
class IngredientInput(BaseModel):
|
|
cas_raw: str
|
|
cas_list: List[str] = Field(default_factory=list)
|
|
percentage: float
|
|
|
|
@model_validator(mode='after')
|
|
def clean_cas(self):
|
|
cleaned = self.cas_raw.replace('\n', '')
|
|
self.cas_list = [c.strip() for c in cleaned.split(';') if c.strip()]
|
|
return self
|
|
|
|
|
|
class Order(BaseModel):
|
|
"""Layer grezzo: riceve l'input, lo valida e lo salva su MongoDB."""
|
|
uuid_ordine: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
client_name: str
|
|
compiler_name: str
|
|
product_type: str
|
|
date: str
|
|
notes: str = ""
|
|
stato: StatoOrdine = StatoOrdine.RICEVUTO
|
|
|
|
ingredients_input: List[IngredientInput]
|
|
total_percentage: float = 0
|
|
num_ingredients: int = 0
|
|
created_at: Optional[str] = None
|
|
|
|
@model_validator(mode='after')
|
|
def set_defaults(self):
|
|
if self.created_at is None:
|
|
self.created_at = dt.now().isoformat()
|
|
if self.total_percentage == 0:
|
|
self.total_percentage = sum(i.percentage for i in self.ingredients_input)
|
|
if self.num_ingredients == 0:
|
|
self.num_ingredients = len(self.ingredients_input)
|
|
return self
|
|
|
|
@classmethod
|
|
def from_input(cls, data: dict):
|
|
"""Costruisce un Order a partire dal JSON grezzo di input."""
|
|
ingredients_input = []
|
|
for ing in data.get('ingredients', []):
|
|
ingredients_input.append(IngredientInput(
|
|
cas_raw=ing.get('CAS Number', ''),
|
|
percentage=ing.get('Percentage (%)', 0)
|
|
))
|
|
|
|
return cls(
|
|
client_name=data.get('client_name', ''),
|
|
compiler_name=data.get('compiler_name', ''),
|
|
product_type=data.get('product_type', ''),
|
|
date=data.get('date', ''),
|
|
notes=data.get('notes', ''),
|
|
ingredients_input=ingredients_input,
|
|
)
|
|
|
|
def save(self):
|
|
"""Salva l'ordine su MongoDB (collection 'orders'). Ritorna il mongo_id."""
|
|
collection = db_connect(collection_name='orders')
|
|
mongo_dict = self.model_dump()
|
|
|
|
result = collection.replace_one(
|
|
{"uuid_ordine": self.uuid_ordine},
|
|
mongo_dict,
|
|
upsert=True
|
|
)
|
|
|
|
if result.upserted_id:
|
|
mongo_id = str(result.upserted_id)
|
|
else:
|
|
doc = collection.find_one({"uuid_ordine": self.uuid_ordine}, {"_id": 1})
|
|
mongo_id = str(doc["_id"])
|
|
|
|
logger.info(f"Ordine {self.uuid_ordine} salvato su MongoDB: {mongo_id}")
|
|
return mongo_id
|
|
|
|
def register(self):
|
|
"""Registra cliente e compilatore su PostgreSQL. Ritorna (id_cliente, id_compilatore)."""
|
|
id_cliente = upsert_cliente(self.client_name)
|
|
id_compilatore = upsert_compilatore(self.compiler_name)
|
|
logger.info(f"Registrato cliente={id_cliente}, compilatore={id_compilatore}")
|
|
return id_cliente, id_compilatore
|
|
|
|
|
|
class Project(BaseModel):
|
|
"""Layer elaborato: contiene gli ingredienti arricchiti, l'esposizione e le statistiche."""
|
|
uuid_progetto: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
uuid_ordine: str
|
|
stato: StatoOrdine = StatoOrdine.VALIDATO
|
|
|
|
ingredients: List[Ingredient] = Field(default_factory=list)
|
|
percentages: Dict[str, float] = Field(default_factory=dict)
|
|
esposition: Optional[Esposition] = None
|
|
|
|
created_at: Optional[str] = None
|
|
|
|
@model_validator(mode='after')
|
|
def set_created_at(self):
|
|
if self.created_at is None:
|
|
self.created_at = dt.now().isoformat()
|
|
return self
|
|
|
|
@classmethod
|
|
def from_order(cls, order: Order):
|
|
"""Crea un progetto a partire da un ordine, estraendo la lista CAS e le percentuali."""
|
|
percentages = {}
|
|
for ing_input in order.ingredients_input:
|
|
for cas in ing_input.cas_list:
|
|
percentages[cas] = ing_input.percentage
|
|
|
|
return cls(
|
|
uuid_ordine=order.uuid_ordine,
|
|
percentages=percentages,
|
|
)
|
|
|
|
def process_ingredients(self):
|
|
"""Arricchisce tutti gli ingredienti tramite Ingredient.get_or_create."""
|
|
self.stato = StatoOrdine.ARRICCHIMENTO
|
|
self.ingredients = []
|
|
errori = 0
|
|
|
|
for cas in self.percentages:
|
|
try:
|
|
ingredient = Ingredient.get_or_create(cas)
|
|
self.ingredients.append(ingredient)
|
|
logger.info(f"Ingrediente {cas} processato")
|
|
except Exception as e:
|
|
logger.error(f"Errore processando CAS {cas}: {e}")
|
|
errori += 1
|
|
|
|
self.stato = StatoOrdine.ARRICCHIMENTO_PARZIALE if errori > 0 else StatoOrdine.ARRICCHITO
|
|
self.save()
|
|
return self.ingredients
|
|
|
|
def set_esposition(self, preset_name: str):
|
|
"""Carica un preset di esposizione da PostgreSQL per nome."""
|
|
presets = Esposition.get_presets()
|
|
for p in presets:
|
|
if p.preset_name == preset_name:
|
|
self.esposition = p
|
|
return p
|
|
logger.warning(f"Preset '{preset_name}' non trovato")
|
|
return None
|
|
|
|
def save(self):
|
|
"""Salva il progetto su MongoDB (collection 'projects'). Ritorna il mongo_id."""
|
|
collection = db_connect(collection_name='projects')
|
|
mongo_dict = self.model_dump()
|
|
|
|
result = collection.replace_one(
|
|
{"uuid_progetto": self.uuid_progetto},
|
|
mongo_dict,
|
|
upsert=True
|
|
)
|
|
|
|
if result.upserted_id:
|
|
mongo_id = str(result.upserted_id)
|
|
else:
|
|
doc = collection.find_one({"uuid_progetto": self.uuid_progetto}, {"_id": 1})
|
|
mongo_id = str(doc["_id"])
|
|
|
|
logger.info(f"Progetto {self.uuid_progetto} salvato su MongoDB: {mongo_id}")
|
|
return mongo_id
|
|
|
|
def get_stats(self):
|
|
"""Ritorna statistiche sul progetto e sullo stato di arricchimento."""
|
|
stats = {
|
|
"uuid_progetto": self.uuid_progetto,
|
|
"uuid_ordine": self.uuid_ordine,
|
|
"stato": self.stato.name,
|
|
"has_esposition": self.esposition is not None,
|
|
"num_ingredients": len(self.ingredients),
|
|
"num_cas_input": len(self.percentages),
|
|
}
|
|
|
|
if self.ingredients:
|
|
stats["enrichment"] = {
|
|
"with_dap": sum(1 for i in self.ingredients if i.dap_info is not None),
|
|
"with_cosing": sum(1 for i in self.ingredients if i.cosing_info is not None),
|
|
"with_tox": sum(1 for i in self.ingredients if i.toxicity is not None),
|
|
"with_noael": sum(
|
|
1 for i in self.ingredients
|
|
if i.toxicity and any(ind.indicator == 'NOAEL' for ind in i.toxicity.indicators)
|
|
),
|
|
}
|
|
|
|
return stats
|