|
@@ -15,7 +15,15 @@ from app.crud.crm.data_scope_helpers import (
|
|
|
apply_crm_create_by_scope,
|
|
apply_crm_create_by_scope,
|
|
|
ensure_crm_create_by_row_in_scope,
|
|
ensure_crm_create_by_row_in_scope,
|
|
|
)
|
|
)
|
|
|
-from app.models.crm_models import CrmProduct, CrmProductCategory, SysOss
|
|
|
|
|
|
|
+from app.models.crm_models import (
|
|
|
|
|
+ CrmProduct,
|
|
|
|
|
+ CrmProductAlias,
|
|
|
|
|
+ CrmProductBrand,
|
|
|
|
|
+ CrmProductCategory,
|
|
|
|
|
+ SysOss,
|
|
|
|
|
+)
|
|
|
|
|
+from app.models.pl_models import PlInventory, PlPlan, PlPlanProduct
|
|
|
|
|
+from fastapi import HTTPException
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 工具函数 ──────────────────────────────────────────────────
|
|
# ── 工具函数 ──────────────────────────────────────────────────
|
|
@@ -282,7 +290,232 @@ def category_code_exists(
|
|
|
# ── 产品 CRUD ────────────────────────────────────────────────
|
|
# ── 产品 CRUD ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _product_row_to_dict(row: CrmProduct) -> Dict[str, Any]:
|
|
|
|
|
|
|
+def _brand_row_to_dict(row: CrmProductBrand) -> Dict[str, Any]:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "id": row.id,
|
|
|
|
|
+ "brandName": row.brand_name,
|
|
|
|
|
+ "status": row.status or "on",
|
|
|
|
|
+ "orderNum": row.order_num if row.order_num is not None else 0,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _alias_row_to_dict(row: CrmProductAlias) -> Dict[str, Any]:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "id": row.id,
|
|
|
|
|
+ "aliasName": row.alias_name,
|
|
|
|
|
+ "orderNum": row.order_num if row.order_num is not None else 0,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _load_brands_aliases_maps(
|
|
|
|
|
+ db: Session, product_ids: List[str], tenant_no: str
|
|
|
|
|
+) -> Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]]:
|
|
|
|
|
+ brands_map: Dict[str, List[Dict[str, Any]]] = {pid: [] for pid in product_ids}
|
|
|
|
|
+ aliases_map: Dict[str, List[Dict[str, Any]]] = {pid: [] for pid in product_ids}
|
|
|
|
|
+ if not product_ids:
|
|
|
|
|
+ return brands_map, aliases_map
|
|
|
|
|
+ brand_rows = (
|
|
|
|
|
+ db.query(CrmProductBrand)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ CrmProductBrand.product_id.in_(product_ids),
|
|
|
|
|
+ CrmProductBrand.tenant_no == tenant_no,
|
|
|
|
|
+ CrmProductBrand.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ .order_by(CrmProductBrand.order_num.asc(), CrmProductBrand.create_time.asc())
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
|
|
+ for row in brand_rows:
|
|
|
|
|
+ brands_map.setdefault(row.product_id, []).append(_brand_row_to_dict(row))
|
|
|
|
|
+ alias_rows = (
|
|
|
|
|
+ db.query(CrmProductAlias)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ CrmProductAlias.product_id.in_(product_ids),
|
|
|
|
|
+ CrmProductAlias.tenant_no == tenant_no,
|
|
|
|
|
+ CrmProductAlias.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ .order_by(CrmProductAlias.order_num.asc(), CrmProductAlias.create_time.asc())
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
|
|
+ for row in alias_rows:
|
|
|
|
|
+ aliases_map.setdefault(row.product_id, []).append(_alias_row_to_dict(row))
|
|
|
|
|
+ return brands_map, aliases_map
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _assert_brand_deletable(db: Session, tenant_no: str, brand_id: str, brand_name: str) -> None:
|
|
|
|
|
+ if not brand_id:
|
|
|
|
|
+ return
|
|
|
|
|
+ inv = (
|
|
|
|
|
+ db.query(PlInventory)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ PlInventory.tenant_no == tenant_no,
|
|
|
|
|
+ PlInventory.brand_id == brand_id,
|
|
|
|
|
+ PlInventory.del_flag == 0,
|
|
|
|
|
+ PlInventory.stock_qty > 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ .first()
|
|
|
|
|
+ )
|
|
|
|
|
+ if inv:
|
|
|
|
|
+ raise HTTPException(
|
|
|
|
|
+ status_code=400,
|
|
|
|
|
+ detail=f"品牌「{brand_name or brand_id}」仍有库存,无法删除",
|
|
|
|
|
+ )
|
|
|
|
|
+ plan_ref = (
|
|
|
|
|
+ db.query(PlPlanProduct)
|
|
|
|
|
+ .join(PlPlan, PlPlan.id == PlPlanProduct.plan_id)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ PlPlanProduct.tenant_no == tenant_no,
|
|
|
|
|
+ PlPlanProduct.brand_id == brand_id,
|
|
|
|
|
+ PlPlanProduct.del_flag == 0,
|
|
|
|
|
+ PlPlan.del_flag == 0,
|
|
|
|
|
+ PlPlan.archive_status != "archived",
|
|
|
|
|
+ )
|
|
|
|
|
+ .first()
|
|
|
|
|
+ )
|
|
|
|
|
+ if plan_ref:
|
|
|
|
|
+ raise HTTPException(
|
|
|
|
|
+ status_code=400,
|
|
|
|
|
+ detail=f"品牌「{brand_name or brand_id}」仍被未归档计划引用,无法删除",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _replace_product_brands(
|
|
|
|
|
+ db: Session,
|
|
|
|
|
+ *,
|
|
|
|
|
+ product_id: str,
|
|
|
|
|
+ tenant_no: str,
|
|
|
|
|
+ user_id: Optional[str],
|
|
|
|
|
+ brands: Optional[List[Any]],
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ if brands is None:
|
|
|
|
|
+ return
|
|
|
|
|
+ existing = (
|
|
|
|
|
+ db.query(CrmProductBrand)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ CrmProductBrand.product_id == product_id,
|
|
|
|
|
+ CrmProductBrand.tenant_no == tenant_no,
|
|
|
|
|
+ CrmProductBrand.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
|
|
+ keep_ids = set()
|
|
|
|
|
+ for item in brands:
|
|
|
|
|
+ data = item if isinstance(item, dict) else item.model_dump(by_alias=False)
|
|
|
|
|
+ bid = (data.get("id") or "").strip() or None
|
|
|
|
|
+ name = (data.get("brand_name") or data.get("brandName") or "").strip()
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if bid:
|
|
|
|
|
+ keep_ids.add(bid)
|
|
|
|
|
+ for row in existing:
|
|
|
|
|
+ if row.id not in keep_ids:
|
|
|
|
|
+ _assert_brand_deletable(db, tenant_no, row.id, row.brand_name)
|
|
|
|
|
+ row.del_flag = 1
|
|
|
|
|
+ row.update_by = user_id
|
|
|
|
|
+ row.update_time = datetime.now()
|
|
|
|
|
+ existing_by_id = {r.id: r for r in existing}
|
|
|
|
|
+ for idx, item in enumerate(brands):
|
|
|
|
|
+ data = item if isinstance(item, dict) else item.model_dump(by_alias=False)
|
|
|
|
|
+ name = (data.get("brand_name") or data.get("brandName") or "").strip()
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ continue
|
|
|
|
|
+ bid = (data.get("id") or "").strip() or None
|
|
|
|
|
+ status = (data.get("status") or "on").strip() or "on"
|
|
|
|
|
+ order_num = data.get("order_num") if data.get("order_num") is not None else data.get("orderNum")
|
|
|
|
|
+ if order_num is None:
|
|
|
|
|
+ order_num = idx
|
|
|
|
|
+ if bid and bid in existing_by_id:
|
|
|
|
|
+ row = existing_by_id[bid]
|
|
|
|
|
+ row.del_flag = 0
|
|
|
|
|
+ row.brand_name = name
|
|
|
|
|
+ row.status = status
|
|
|
|
|
+ row.order_num = order_num
|
|
|
|
|
+ row.update_by = user_id
|
|
|
|
|
+ row.update_time = datetime.now()
|
|
|
|
|
+ else:
|
|
|
|
|
+ db.add(
|
|
|
|
|
+ CrmProductBrand(
|
|
|
|
|
+ id=generate_uuid(),
|
|
|
|
|
+ product_id=product_id,
|
|
|
|
|
+ brand_name=name,
|
|
|
|
|
+ status=status,
|
|
|
|
|
+ tenant_no=tenant_no,
|
|
|
|
|
+ order_num=order_num,
|
|
|
|
|
+ create_by=user_id,
|
|
|
|
|
+ create_dept_id="",
|
|
|
|
|
+ del_flag=0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _replace_product_aliases(
|
|
|
|
|
+ db: Session,
|
|
|
|
|
+ *,
|
|
|
|
|
+ product_id: str,
|
|
|
|
|
+ tenant_no: str,
|
|
|
|
|
+ user_id: Optional[str],
|
|
|
|
|
+ aliases: Optional[List[Any]],
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ if aliases is None:
|
|
|
|
|
+ return
|
|
|
|
|
+ existing = (
|
|
|
|
|
+ db.query(CrmProductAlias)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ CrmProductAlias.product_id == product_id,
|
|
|
|
|
+ CrmProductAlias.tenant_no == tenant_no,
|
|
|
|
|
+ CrmProductAlias.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
|
|
+ existing_by_id = {r.id: r for r in existing}
|
|
|
|
|
+ keep_ids = set()
|
|
|
|
|
+ for item in aliases:
|
|
|
|
|
+ data = item if isinstance(item, dict) else item.model_dump(by_alias=False)
|
|
|
|
|
+ aid = (data.get("id") or "").strip() or None
|
|
|
|
|
+ name = (data.get("alias_name") or data.get("aliasName") or "").strip()
|
|
|
|
|
+ if name and aid:
|
|
|
|
|
+ keep_ids.add(aid)
|
|
|
|
|
+ for row in existing:
|
|
|
|
|
+ if row.id not in keep_ids:
|
|
|
|
|
+ row.del_flag = 1
|
|
|
|
|
+ row.update_by = user_id
|
|
|
|
|
+ row.update_time = datetime.now()
|
|
|
|
|
+ for idx, item in enumerate(aliases):
|
|
|
|
|
+ data = item if isinstance(item, dict) else item.model_dump(by_alias=False)
|
|
|
|
|
+ name = (data.get("alias_name") or data.get("aliasName") or "").strip()
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ continue
|
|
|
|
|
+ aid = (data.get("id") or "").strip() or None
|
|
|
|
|
+ order_num = data.get("order_num") if data.get("order_num") is not None else data.get("orderNum")
|
|
|
|
|
+ if order_num is None:
|
|
|
|
|
+ order_num = idx
|
|
|
|
|
+ if aid and aid in existing_by_id:
|
|
|
|
|
+ row = existing_by_id[aid]
|
|
|
|
|
+ row.del_flag = 0
|
|
|
|
|
+ row.alias_name = name
|
|
|
|
|
+ row.order_num = order_num
|
|
|
|
|
+ row.update_by = user_id
|
|
|
|
|
+ row.update_time = datetime.now()
|
|
|
|
|
+ else:
|
|
|
|
|
+ db.add(
|
|
|
|
|
+ CrmProductAlias(
|
|
|
|
|
+ id=generate_uuid(),
|
|
|
|
|
+ product_id=product_id,
|
|
|
|
|
+ alias_name=name,
|
|
|
|
|
+ tenant_no=tenant_no,
|
|
|
|
|
+ order_num=order_num,
|
|
|
|
|
+ create_by=user_id,
|
|
|
|
|
+ create_dept_id="",
|
|
|
|
|
+ del_flag=0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _product_row_to_dict(
|
|
|
|
|
+ row: CrmProduct,
|
|
|
|
|
+ *,
|
|
|
|
|
+ brands: Optional[List[Dict[str, Any]]] = None,
|
|
|
|
|
+ aliases: Optional[List[Dict[str, Any]]] = None,
|
|
|
|
|
+) -> Dict[str, Any]:
|
|
|
"""产品 ORM 对象转 dict(camelCase 键名)"""
|
|
"""产品 ORM 对象转 dict(camelCase 键名)"""
|
|
|
unit = (row.unit or "").strip()
|
|
unit = (row.unit or "").strip()
|
|
|
return {
|
|
return {
|
|
@@ -308,11 +541,23 @@ def _product_row_to_dict(row: CrmProduct) -> Dict[str, Any]:
|
|
|
"saleCount": row.sale_count if row.sale_count is not None else 0,
|
|
"saleCount": row.sale_count if row.sale_count is not None else 0,
|
|
|
"orderNum": row.order_num if row.order_num is not None else 0,
|
|
"orderNum": row.order_num if row.order_num is not None else 0,
|
|
|
"remark": row.remark or "",
|
|
"remark": row.remark or "",
|
|
|
|
|
+ "brands": brands if brands is not None else [],
|
|
|
|
|
+ "aliases": aliases if aliases is not None else [],
|
|
|
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
|
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
|
|
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
|
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def product_to_dict(db: Session, row: CrmProduct) -> Dict[str, Any]:
|
|
|
|
|
+ """单产品详情(含品牌/别名)"""
|
|
|
|
|
+ brands_map, aliases_map = _load_brands_aliases_maps(db, [row.id], row.tenant_no)
|
|
|
|
|
+ return _product_row_to_dict(
|
|
|
|
|
+ row,
|
|
|
|
|
+ brands=brands_map.get(row.id, []),
|
|
|
|
|
+ aliases=aliases_map.get(row.id, []),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def list_products(
|
|
def list_products(
|
|
|
db: Session,
|
|
db: Session,
|
|
|
tenant_no: str,
|
|
tenant_no: str,
|
|
@@ -375,7 +620,16 @@ def list_products(
|
|
|
.limit(page_size)
|
|
.limit(page_size)
|
|
|
.all()
|
|
.all()
|
|
|
)
|
|
)
|
|
|
- return [_product_row_to_dict(r) for r in rows], int(total)
|
|
|
|
|
|
|
+ ids = [r.id for r in rows]
|
|
|
|
|
+ brands_map, aliases_map = _load_brands_aliases_maps(db, ids, tenant_no)
|
|
|
|
|
+ return [
|
|
|
|
|
+ _product_row_to_dict(
|
|
|
|
|
+ r,
|
|
|
|
|
+ brands=brands_map.get(r.id, []),
|
|
|
|
|
+ aliases=aliases_map.get(r.id, []),
|
|
|
|
|
+ )
|
|
|
|
|
+ for r in rows
|
|
|
|
|
+ ], int(total)
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_product(db: Session, product_id: str) -> Optional[CrmProduct]:
|
|
def get_product(db: Session, product_id: str) -> Optional[CrmProduct]:
|
|
@@ -525,6 +779,21 @@ def create_product(
|
|
|
del_flag=0,
|
|
del_flag=0,
|
|
|
)
|
|
)
|
|
|
db.add(obj)
|
|
db.add(obj)
|
|
|
|
|
+ db.flush()
|
|
|
|
|
+ _replace_product_brands(
|
|
|
|
|
+ db,
|
|
|
|
|
+ product_id=obj.id,
|
|
|
|
|
+ tenant_no=tenant_no,
|
|
|
|
|
+ user_id=user_id,
|
|
|
|
|
+ brands=kwargs.get("brands"),
|
|
|
|
|
+ )
|
|
|
|
|
+ _replace_product_aliases(
|
|
|
|
|
+ db,
|
|
|
|
|
+ product_id=obj.id,
|
|
|
|
|
+ tenant_no=tenant_no,
|
|
|
|
|
+ user_id=user_id,
|
|
|
|
|
+ aliases=kwargs.get("aliases"),
|
|
|
|
|
+ )
|
|
|
db.commit()
|
|
db.commit()
|
|
|
db.refresh(obj)
|
|
db.refresh(obj)
|
|
|
return obj
|
|
return obj
|
|
@@ -585,6 +854,23 @@ def update_product(
|
|
|
else:
|
|
else:
|
|
|
obj.category_name = ""
|
|
obj.category_name = ""
|
|
|
|
|
|
|
|
|
|
+ if "brands" in kwargs:
|
|
|
|
|
+ _replace_product_brands(
|
|
|
|
|
+ db,
|
|
|
|
|
+ product_id=obj.id,
|
|
|
|
|
+ tenant_no=obj.tenant_no,
|
|
|
|
|
+ user_id=user_id,
|
|
|
|
|
+ brands=kwargs.get("brands"),
|
|
|
|
|
+ )
|
|
|
|
|
+ if "aliases" in kwargs:
|
|
|
|
|
+ _replace_product_aliases(
|
|
|
|
|
+ db,
|
|
|
|
|
+ product_id=obj.id,
|
|
|
|
|
+ tenant_no=obj.tenant_no,
|
|
|
|
|
+ user_id=user_id,
|
|
|
|
|
+ aliases=kwargs.get("aliases"),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
obj.update_by = user_id
|
|
obj.update_by = user_id
|
|
|
obj.update_time = datetime.now()
|
|
obj.update_time = datetime.now()
|
|
|
db.commit()
|
|
db.commit()
|