116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
"""Feed importer: normalize -> validate -> upsert.
|
|
|
|
NEVER auto-publishes products: new products are created with status 'imported'
|
|
only. Invalid items (missing sku/title, non-positive cost, negative inventory)
|
|
are skipped and logged to audit_logs.
|
|
"""
|
|
from decimal import Decimal
|
|
|
|
from app.engines.suppliers.base import build_adapter
|
|
from app.models import AuditLog, Product, SupplierProduct
|
|
|
|
|
|
def validate(item):
|
|
problems = []
|
|
if not (item.get("sku") or "").strip():
|
|
problems.append("missing sku")
|
|
if not (item.get("title") or "").strip():
|
|
problems.append("missing title")
|
|
try:
|
|
cost = float(item.get("cost") or 0)
|
|
if cost <= 0:
|
|
problems.append("non-positive cost")
|
|
except (TypeError, ValueError):
|
|
problems.append("invalid cost")
|
|
try:
|
|
inventory = int(float(item.get("inventory") or 0))
|
|
if inventory < 0:
|
|
problems.append("negative inventory")
|
|
except (TypeError, ValueError):
|
|
problems.append("invalid inventory")
|
|
return problems
|
|
|
|
|
|
def import_feed(db, supplier, feed_type=None, data_or_url=""):
|
|
"""Import a supplier feed into products + supplier_products.
|
|
|
|
Returns summary dict: {supplier, received, imported, updated, skipped: [...]}
|
|
"""
|
|
adapter = build_adapter(supplier, data_or_url=data_or_url)
|
|
items = adapter.get_products()
|
|
|
|
summary = {
|
|
"supplier": supplier.name,
|
|
"supplier_id": str(supplier.id),
|
|
"received": len(items),
|
|
"imported": 0,
|
|
"updated": 0,
|
|
"skipped": [],
|
|
}
|
|
|
|
for item in items:
|
|
problems = validate(item)
|
|
if problems:
|
|
reason = "; ".join(problems)
|
|
summary["skipped"].append({"sku": item.get("sku"), "reason": reason})
|
|
db.add(
|
|
AuditLog(
|
|
actor="importer",
|
|
action="import_skipped",
|
|
entity="product",
|
|
entity_id=item.get("sku"),
|
|
detail={"supplier": supplier.name, "reason": reason},
|
|
)
|
|
)
|
|
continue
|
|
|
|
product = db.query(Product).filter(Product.sku == item["sku"]).first()
|
|
if product is None:
|
|
product = Product(sku=item["sku"], status="imported")
|
|
db.add(product)
|
|
summary["imported"] += 1
|
|
else:
|
|
summary["updated"] += 1
|
|
|
|
# set all required fields BEFORE flushing (title is NOT NULL in schema)
|
|
product.title = item["title"]
|
|
product.description = item.get("description") or product.description
|
|
product.category = item.get("category") or product.category
|
|
product.brand = item.get("brand") or product.brand
|
|
product.cost = Decimal(str(item["cost"]))
|
|
|
|
if item.get("image_url"):
|
|
images = list(product.images or [])
|
|
if item["image_url"] not in images:
|
|
images.append(item["image_url"])
|
|
product.images = images
|
|
|
|
db.flush() # product.id available now
|
|
|
|
sp = (
|
|
db.query(SupplierProduct)
|
|
.filter(
|
|
SupplierProduct.supplier_id == supplier.id,
|
|
SupplierProduct.supplier_sku == item["sku"],
|
|
)
|
|
.first()
|
|
)
|
|
if sp is None:
|
|
sp = SupplierProduct(
|
|
supplier_id=supplier.id,
|
|
supplier_sku=item["sku"],
|
|
product_id=product.id,
|
|
)
|
|
db.add(sp)
|
|
db.flush()
|
|
else:
|
|
sp.product_id = product.id
|
|
|
|
sp.supplier_cost = Decimal(str(item["cost"]))
|
|
sp.supplier_inventory = int(item.get("inventory") or 0)
|
|
sp.shipping_cost = Decimal(str(item.get("shipping_cost") or 0))
|
|
sp.shipping_time = item.get("shipping_time")
|
|
|
|
db.commit()
|
|
return summary
|