mini_supplier.py 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313
  1. """小程序端供应商接口"""
  2. from decimal import Decimal
  3. from typing import Optional
  4. from datetime import datetime
  5. from fastapi import APIRouter, Depends, Request, Query
  6. from sqlalchemy.orm import Session
  7. from app.core.database import get_db
  8. from app.core.auth import get_current_user, get_client_ip
  9. from app.core.response import success_response, error_response
  10. from app.models.models import BasAddress
  11. from app.crud.address import address_crud
  12. from app.crud.user import user_crud
  13. from app.crud.region import region_crud
  14. from app.crud.attachment import attachment_crud
  15. from app.models.models import BizAttachment
  16. router = APIRouter(prefix="/api/mini/supplier", tags=["小程序端-供应商接口"])
  17. def _supplier_quote_unit_yuan_from_pli(pli) -> Optional[float]:
  18. """价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。"""
  19. if pli is None or getattr(pli, "price", None) is None:
  20. return None
  21. final_price = Decimal(str(pli.price))
  22. raw_coeff = getattr(pli, "pricing_coefficient", None)
  23. if raw_coeff is None:
  24. coeff_pct = Decimal("0")
  25. else:
  26. try:
  27. coeff_pct = Decimal(str(raw_coeff))
  28. except Exception:
  29. coeff_pct = Decimal("0")
  30. factor = Decimal("1") + coeff_pct / Decimal("100")
  31. if factor <= Decimal("0"):
  32. factor = Decimal("1")
  33. supplier_unit = (final_price / factor).quantize(Decimal("0.01"))
  34. return float(supplier_unit)
  35. def require_supplier(current_user: dict = Depends(get_current_user)):
  36. """验证供应商权限"""
  37. if current_user.get("role") != "supplier":
  38. return error_response(403, "需要供应商权限")
  39. return current_user
  40. def _normalize_image_urls(raw) -> list[str]:
  41. out: list[str] = []
  42. if isinstance(raw, list):
  43. for x in raw:
  44. s = str(x or "").strip()
  45. if s:
  46. out.append(s)
  47. elif isinstance(raw, str):
  48. s = raw.strip()
  49. if s:
  50. out.append(s)
  51. return out
  52. def _replace_delivery_prepare_attachments(
  53. db: Session,
  54. *,
  55. task_id: str,
  56. image_urls: list[str],
  57. uploader_id: Optional[str],
  58. uploader_name: Optional[str],
  59. ):
  60. rows = db.query(BizAttachment).filter(
  61. BizAttachment.business_type == "delivery_prepare",
  62. BizAttachment.business_id == task_id,
  63. BizAttachment.is_del == 0,
  64. ).all()
  65. now = datetime.now()
  66. for r in rows:
  67. r.is_del = 1
  68. r.update_at = now
  69. r.update_by = uploader_id
  70. db.flush()
  71. for idx, url in enumerate(image_urls, start=1):
  72. attachment_crud.create_attachment(
  73. db=db,
  74. business_type="delivery_prepare",
  75. business_id=task_id,
  76. file_name=f"delivery_prepare_{idx}.jpg",
  77. file_url=url,
  78. file_type="image/jpeg",
  79. upload_by=uploader_id,
  80. upload_by_name=uploader_name,
  81. )
  82. # ============ 地址管理 ============
  83. @router.get("/addresses", response_model=dict)
  84. def list_addresses(
  85. db: Session = Depends(get_db),
  86. current_user: dict = Depends(require_supplier)
  87. ):
  88. """获取供应商取货地址列表"""
  89. addresses = address_crud.get_by_user(db, current_user.get("user_id"))
  90. return success_response([{
  91. "id": addr.id,
  92. "userId": addr.user_id,
  93. "addressType": addr.address_type,
  94. "contactPerson": addr.contact_person,
  95. "contactPhone": addr.contact_phone,
  96. "province": addr.province,
  97. "city": addr.city,
  98. "district": addr.district,
  99. "detailAddress": addr.detail_address,
  100. "isDefault": addr.is_default,
  101. "createAt": str(addr.create_at) if addr.create_at else None
  102. } for addr in addresses])
  103. @router.get("/pickup-addresses", response_model=dict)
  104. def list_pickup_addresses(
  105. db: Session = Depends(get_db),
  106. current_user: dict = Depends(require_supplier)
  107. ):
  108. """获取供应商取货地址列表"""
  109. addresses = address_crud.get_by_user_and_type(db, current_user.get("user_id"), "pickup")
  110. return success_response([{
  111. "id": addr.id,
  112. "userId": addr.user_id,
  113. "addressType": addr.address_type,
  114. "contactPerson": addr.contact_person,
  115. "contactPhone": addr.contact_phone,
  116. "province": addr.province,
  117. "city": addr.city,
  118. "district": addr.district,
  119. "detailAddress": addr.detail_address,
  120. "isDefault": addr.is_default
  121. } for addr in addresses])
  122. @router.get("/addresses/{address_id}", response_model=dict)
  123. def get_address(
  124. address_id: str,
  125. db: Session = Depends(get_db),
  126. current_user: dict = Depends(require_supplier)
  127. ):
  128. """获取地址详情"""
  129. address = address_crud.get(db, address_id)
  130. if not address or address.user_id != current_user.get("user_id"):
  131. return error_response(404, "地址不存在")
  132. return success_response({
  133. "id": address.id,
  134. "userId": address.user_id,
  135. "addressType": address.address_type,
  136. "contactPerson": address.contact_person,
  137. "contactPhone": address.contact_phone,
  138. "province": address.province,
  139. "city": address.city,
  140. "district": address.district,
  141. "detailAddress": address.detail_address,
  142. "isDefault": address.is_default,
  143. "createAt": str(address.create_at) if address.create_at else None
  144. })
  145. @router.post("/addresses", response_model=dict)
  146. def create_address(
  147. request: dict,
  148. db: Session = Depends(get_db),
  149. current_user: dict = Depends(require_supplier)
  150. ):
  151. """创建地址"""
  152. request["user_id"] = current_user.get("user_id")
  153. creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  154. # 如果设置为默认,先取消其他默认
  155. if request.get("isDefault"):
  156. address_crud.set_default(db, current_user.get("user_id"), "")
  157. address = address_crud.create(db, request, creator)
  158. return success_response({
  159. "id": address.id,
  160. "addressType": address.address_type,
  161. "contactPerson": address.contact_person,
  162. "contactPhone": address.contact_phone
  163. })
  164. @router.put("/addresses/{address_id}", response_model=dict)
  165. def update_address(
  166. address_id: str,
  167. request: dict,
  168. db: Session = Depends(get_db),
  169. current_user: dict = Depends(require_supplier)
  170. ):
  171. """更新地址"""
  172. address = address_crud.get(db, address_id)
  173. if not address or address.user_id != current_user.get("user_id"):
  174. return error_response(404, "地址不存在")
  175. # 如果设置为默认,先取消其他默认
  176. if request.get("isDefault"):
  177. address_crud.set_default(db, current_user.get("user_id"), address_id)
  178. updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  179. address = address_crud.update_with_history(db, address_id, request, updater)
  180. return success_response({
  181. "id": address.id,
  182. "addressType": address.address_type,
  183. "contactPerson": address.contact_person,
  184. "contactPhone": address.contact_phone
  185. })
  186. @router.delete("/addresses/{address_id}", response_model=dict)
  187. def delete_address(
  188. address_id: str,
  189. db: Session = Depends(get_db),
  190. current_user: dict = Depends(require_supplier)
  191. ):
  192. """删除地址"""
  193. address = address_crud.get(db, address_id)
  194. if not address or address.user_id != current_user.get("user_id"):
  195. return error_response(404, "地址不存在")
  196. success = address_crud.soft_delete_with_history(db, address_id)
  197. return success_response({"id": address_id})
  198. @router.post("/addresses/{address_id}/set-default", response_model=dict)
  199. def set_default_address(
  200. address_id: str,
  201. db: Session = Depends(get_db),
  202. current_user: dict = Depends(require_supplier)
  203. ):
  204. """设置默认地址"""
  205. address = address_crud.get(db, address_id)
  206. if not address or address.user_id != current_user.get("user_id"):
  207. return error_response(404, "地址不存在")
  208. success = address_crud.set_default(db, current_user.get("user_id"), address_id)
  209. if success:
  210. return success_response({"message": "设置成功"})
  211. return error_response(400, "设置失败")
  212. # ============ 个人资料 ============
  213. @router.get("/profile", response_model=dict)
  214. def get_profile(
  215. db: Session = Depends(get_db),
  216. current_user: dict = Depends(require_supplier)
  217. ):
  218. """获取供应商个人资料"""
  219. user = user_crud.get(db, current_user.get("user_id"))
  220. if not user:
  221. return error_response(404, "用户不存在")
  222. region_name = None
  223. if user.region_id:
  224. region = region_crud.get(db, user.region_id)
  225. region_name = region.name if region else None
  226. # 获取默认取货地址
  227. default_address = address_crud.get_default(db, user.id, "pickup")
  228. address_info = None
  229. if default_address:
  230. address_info = {
  231. "contactPerson": default_address.contact_person,
  232. "contactPhone": default_address.contact_phone,
  233. "province": default_address.province,
  234. "city": default_address.city,
  235. "district": default_address.district,
  236. "detailAddress": default_address.detail_address
  237. }
  238. return success_response({
  239. "id": user.id,
  240. "username": user.username,
  241. "realName": user.real_name,
  242. "phone": user.phone,
  243. "role": user.role,
  244. "regionId": user.region_id,
  245. "regionName": region_name,
  246. "address": address_info
  247. })
  248. @router.put("/profile", response_model=dict)
  249. def update_profile(
  250. request: dict,
  251. db: Session = Depends(get_db),
  252. current_user: dict = Depends(require_supplier)
  253. ):
  254. """更新供应商个人资料"""
  255. user = user_crud.get(db, current_user.get("user_id"))
  256. if not user:
  257. return error_response(404, "用户不存在")
  258. updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  259. user = user_crud.update_with_history(db, current_user.get("user_id"), request, updater)
  260. return success_response({
  261. "id": user.id,
  262. "realName": user.real_name,
  263. "phone": user.phone
  264. })
  265. # ============ 询价单管理 ============
  266. @router.get("/inquiries", response_model=dict)
  267. def list_inquiries(
  268. page: int = Query(1, ge=1, description="页码"),
  269. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  270. db: Session = Depends(get_db),
  271. current_user: dict = Depends(require_supplier)
  272. ):
  273. """获取供应商可响应的询价单列表"""
  274. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  275. from app.crud.quotation import quotation_crud
  276. user = user_crud.get(db, current_user.get("user_id"))
  277. if not user or not user.region_id:
  278. return error_response(400, "用户未绑定区域")
  279. inquiries, total = inquiry_crud.get_multi_with_filters(
  280. db,
  281. region_id=user.region_id,
  282. status="published",
  283. skip=(page - 1) * pageSize,
  284. limit=pageSize
  285. )
  286. inquiry_list = []
  287. for inquiry in inquiries:
  288. has_quoted = quotation_crud.get_latest_by_inquiry_and_supplier(db, inquiry.id, user.id) is not None
  289. inquiry_list.append({
  290. "id": inquiry.id,
  291. "title": inquiry.title,
  292. "regionId": inquiry.region_id,
  293. "status": inquiry.status,
  294. "publishTime": str(inquiry.publish_time) if inquiry.publish_time else None,
  295. "closeTime": str(inquiry.close_time) if inquiry.close_time else None,
  296. "remark": inquiry.remark,
  297. "createAt": str(inquiry.create_at) if inquiry.create_at else None,
  298. "hasQuoted": has_quoted
  299. })
  300. return success_response({
  301. "list": inquiry_list,
  302. "total": total,
  303. "page": page,
  304. "pageSize": pageSize,
  305. "totalPages": (total + pageSize - 1) // pageSize
  306. })
  307. @router.get("/inquiries/{inquiry_id}", response_model=dict)
  308. def get_inquiry_detail(
  309. inquiry_id: str,
  310. db: Session = Depends(get_db),
  311. current_user: dict = Depends(require_supplier)
  312. ):
  313. """获取询价单详情"""
  314. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  315. from app.crud.product import product_crud
  316. from app.crud.category import category_crud
  317. from app.crud.unit import unit_crud
  318. from app.crud.quotation import quotation_crud
  319. user = user_crud.get(db, current_user.get("user_id"))
  320. if not user or not user.region_id:
  321. return error_response(400, "用户未绑定区域")
  322. inquiry = inquiry_crud.get(db, inquiry_id)
  323. if not inquiry:
  324. return error_response(404, "询价单不存在")
  325. if inquiry.region_id != user.region_id:
  326. return error_response(403, "无权查看此询价单")
  327. items = inquiry_item_crud.get_by_inquiry(db, inquiry_id)
  328. quotation = quotation_crud.get_latest_by_inquiry_and_supplier(db, inquiry_id, user.id)
  329. latest_quote_price_map = {}
  330. if quotation:
  331. from app.crud.quotation import quotation_item_crud
  332. quotation_items = quotation_item_crud.get_by_quotation(db, quotation.id)
  333. for quote_item in quotation_items:
  334. latest_quote_price_map[quote_item.product_id] = float(quote_item.price) if quote_item.price else 0
  335. item_details = []
  336. for item in items:
  337. product = product_crud.get(db, item.product_id)
  338. category_name = None
  339. first_category_id = None
  340. first_category_name = None
  341. second_category_id = None
  342. second_category_name = None
  343. unit_name = None
  344. if product and product.category_id:
  345. category = category_crud.get(db, product.category_id)
  346. category_name = category.name if category else None
  347. if category:
  348. if category.parent_id:
  349. parent_category = category_crud.get(db, category.parent_id)
  350. first_category_id = parent_category.id if parent_category else category.id
  351. first_category_name = parent_category.name if parent_category else category.name
  352. second_category_id = category.id
  353. second_category_name = category.name
  354. else:
  355. first_category_id = category.id
  356. first_category_name = category.name
  357. if product and product.unit_id:
  358. unit = unit_crud.get(db, product.unit_id)
  359. unit_name = unit.name if unit else None
  360. item_details.append({
  361. "id": item.id,
  362. "productId": item.product_id,
  363. "productName": product.name if product else None,
  364. "description": product.description if product else None,
  365. "imageUrl": product.image_url if product else None,
  366. "categoryId": product.category_id if product else None,
  367. "categoryName": category_name,
  368. "firstCategoryId": first_category_id,
  369. "firstCategoryName": first_category_name,
  370. "secondCategoryId": second_category_id,
  371. "secondCategoryName": second_category_name,
  372. "unitId": product.unit_id if product else None,
  373. "unitName": unit_name,
  374. "measureValue": float(product.measure_value) if product and product.measure_value is not None else None,
  375. "lastPrice": latest_quote_price_map.get(item.product_id),
  376. "createAt": str(item.create_at) if item.create_at else None
  377. })
  378. return success_response({
  379. "id": inquiry.id,
  380. "title": inquiry.title,
  381. "regionId": inquiry.region_id,
  382. "status": inquiry.status,
  383. "publishTime": str(inquiry.publish_time) if inquiry.publish_time else None,
  384. "closeTime": str(inquiry.close_time) if inquiry.close_time else None,
  385. "remark": inquiry.remark,
  386. "createAt": str(inquiry.create_at) if inquiry.create_at else None,
  387. "items": item_details,
  388. "hasQuoted": quotation is not None,
  389. "quotationId": quotation.id if quotation else None,
  390. "quotationStatus": quotation.status if quotation else None
  391. })
  392. # ============ 报价单管理 ============
  393. @router.post("/quotations", response_model=dict)
  394. def submit_quotation(
  395. request: dict,
  396. db: Session = Depends(get_db),
  397. current_user: dict = Depends(require_supplier)
  398. ):
  399. """提交报价单"""
  400. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  401. from app.crud.quotation import quotation_crud, quotation_item_crud
  402. from decimal import Decimal
  403. user = user_crud.get(db, current_user.get("user_id"))
  404. if not user or not user.region_id:
  405. return error_response(400, "用户未绑定区域")
  406. inquiry_id = request.get("inquiryId")
  407. items = request.get("items", [])
  408. if not inquiry_id:
  409. return error_response(400, "询价单ID不能为空")
  410. if not items:
  411. return error_response(400, "报价项不能为空")
  412. inquiry = inquiry_crud.get(db, inquiry_id)
  413. if not inquiry:
  414. return error_response(404, "询价单不存在")
  415. if inquiry.region_id != user.region_id:
  416. return error_response(403, "无权对此询价单报价")
  417. if inquiry.status != "published":
  418. return error_response(400, "询价单已关闭")
  419. # 支持同一询价单多次报价:每次提交都创建一张新报价单
  420. inquiry_items = inquiry_item_crud.get_by_inquiry(db, inquiry_id)
  421. inquiry_product_ids = set(item.product_id for item in inquiry_items)
  422. submitted_product_ids = set(item.get("productId") for item in items)
  423. if submitted_product_ids != inquiry_product_ids:
  424. return error_response(400, "报价商品必须与询价商品一致")
  425. creator = {"user_id": user.id, "real_name": user.real_name}
  426. quotation_data = {
  427. "inquiry_id": inquiry_id,
  428. "supplier_id": user.id,
  429. "region_id": user.region_id,
  430. "status": "submitted",
  431. "submit_time": datetime.now()
  432. }
  433. quotation_items_data = []
  434. total_amount = Decimal("0")
  435. for item in items:
  436. price = Decimal(str(item.get("price", 0)))
  437. total_amount += price
  438. quotation_items_data.append({
  439. "product_id": item.get("productId"),
  440. "unit_id": item.get("unitId"),
  441. "price": price
  442. })
  443. quotation_data["total_amount"] = total_amount
  444. quotation = quotation_crud.create(db, quotation_data, creator)
  445. quotation_item_crud.create_items(db, quotation.id, quotation_items_data, creator)
  446. return success_response({
  447. "id": quotation.id,
  448. "totalAmount": float(quotation.total_amount),
  449. "status": quotation.status,
  450. "submitTime": str(quotation.submit_time) if quotation.submit_time else None
  451. })
  452. @router.get("/quotations", response_model=dict)
  453. def list_my_quotations(
  454. page: int = Query(1, ge=1, description="页码"),
  455. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  456. inquiryId: Optional[str] = Query(None, description="询价单ID"),
  457. status: Optional[str] = Query(None, description="报价状态,如 selected=已采纳"),
  458. db: Session = Depends(get_db),
  459. current_user: dict = Depends(require_supplier)
  460. ):
  461. """获取我的报价列表"""
  462. from app.crud.inquiry import inquiry_crud
  463. from app.crud.quotation import quotation_crud
  464. user = user_crud.get(db, current_user.get("user_id"))
  465. if not user:
  466. return error_response(404, "用户不存在")
  467. quotations, total = quotation_crud.get_multi_with_filters(
  468. db,
  469. supplier_id=user.id,
  470. inquiry_id=inquiryId,
  471. status=status,
  472. skip=(page - 1) * pageSize,
  473. limit=pageSize
  474. )
  475. inquiry_map = {}
  476. quotation_list = []
  477. for q in quotations:
  478. if q.inquiry_id and q.inquiry_id not in inquiry_map:
  479. inquiry = inquiry_crud.get(db, q.inquiry_id)
  480. inquiry_map[q.inquiry_id] = inquiry.title if inquiry else None
  481. quotation_list.append({
  482. "id": q.id,
  483. "inquiryId": q.inquiry_id,
  484. "inquiryTitle": inquiry_map.get(q.inquiry_id),
  485. "status": q.status,
  486. "totalAmount": float(q.total_amount) if q.total_amount else 0,
  487. "submitTime": str(q.submit_time) if q.submit_time else None,
  488. })
  489. return success_response({
  490. "list": quotation_list,
  491. "total": total,
  492. "page": page,
  493. "pageSize": pageSize,
  494. "totalPages": (total + pageSize - 1) // pageSize
  495. })
  496. @router.get("/quotations/{quotation_id}", response_model=dict)
  497. def get_my_quotation(
  498. quotation_id: str,
  499. db: Session = Depends(get_db),
  500. current_user: dict = Depends(require_supplier)
  501. ):
  502. """获取我的报价单详情"""
  503. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  504. from app.crud.quotation import quotation_crud, quotation_item_crud
  505. from app.crud.product import product_crud
  506. user = user_crud.get(db, current_user.get("user_id"))
  507. if not user:
  508. return error_response(404, "用户不存在")
  509. quotation = quotation_crud.get(db, quotation_id)
  510. if not quotation:
  511. return error_response(404, "报价单不存在")
  512. if quotation.supplier_id != user.id:
  513. return error_response(403, "无权查看此报价单")
  514. items = quotation_item_crud.get_by_quotation(db, quotation_id)
  515. inquiry_title = None
  516. if quotation.inquiry_id:
  517. inquiry = inquiry_crud.get(db, quotation.inquiry_id)
  518. inquiry_title = inquiry.title if inquiry else None
  519. item_details = []
  520. for item in items:
  521. product = product_crud.get(db, item.product_id)
  522. item_details.append({
  523. "id": item.id,
  524. "productId": item.product_id,
  525. "productName": product.name if product else None,
  526. "unitId": item.unit_id,
  527. "price": float(item.price) if item.price else 0,
  528. "createAt": str(item.create_at) if item.create_at else None
  529. })
  530. return success_response({
  531. "id": quotation.id,
  532. "inquiryId": quotation.inquiry_id,
  533. "inquiryTitle": inquiry_title,
  534. "status": quotation.status,
  535. "totalAmount": float(quotation.total_amount) if quotation.total_amount else 0,
  536. "submitTime": str(quotation.submit_time) if quotation.submit_time else None,
  537. "items": item_details
  538. })
  539. # ============ 价格清单 - 供应商查看 ============
  540. @router.get("/price-lists", response_model=dict)
  541. def list_my_price_lists(
  542. page: int = Query(1, ge=1, description="页码"),
  543. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  544. status: Optional[str] = Query(None, description="状态"),
  545. db: Session = Depends(get_db),
  546. current_user: dict = Depends(require_supplier)
  547. ):
  548. """获取供应商的价格清单列表"""
  549. from app.crud.price_list import price_list_crud
  550. from app.crud.region import region_crud
  551. user = user_crud.get(db, current_user.get("user_id"))
  552. if not user:
  553. return error_response(404, "用户不存在")
  554. price_lists, total = price_list_crud.get_multi_with_filters(
  555. db,
  556. supplier_id=user.id,
  557. status=status,
  558. skip=(page - 1) * pageSize,
  559. limit=pageSize
  560. )
  561. region_map = {}
  562. for pl in price_lists:
  563. if pl.region_id and pl.region_id not in region_map:
  564. region = region_crud.get(db, pl.region_id)
  565. region_map[pl.region_id] = region.name if region else None
  566. return success_response({
  567. "list": [{
  568. "id": pl.id,
  569. "regionId": pl.region_id,
  570. "regionName": region_map.get(pl.region_id),
  571. "version": pl.version,
  572. "status": pl.status,
  573. "effectiveTime": str(pl.effective_time) if pl.effective_time else None,
  574. "publishedTime": str(pl.published_time) if pl.published_time else None,
  575. "remark": pl.remark,
  576. "createAt": str(pl.create_at) if pl.create_at else None
  577. } for pl in price_lists],
  578. "total": total,
  579. "page": page,
  580. "pageSize": pageSize,
  581. "totalPages": (total + pageSize - 1) // pageSize
  582. })
  583. @router.get("/price-lists/{price_list_id}", response_model=dict)
  584. def get_my_price_list_detail(
  585. price_list_id: str,
  586. db: Session = Depends(get_db),
  587. current_user: dict = Depends(require_supplier)
  588. ):
  589. """获取供应商价格清单详情"""
  590. from app.crud.price_list import price_list_crud, price_list_item_crud
  591. from app.crud.region import region_crud
  592. from app.crud.product import product_crud
  593. from app.crud.unit import unit_crud
  594. user = user_crud.get(db, current_user.get("user_id"))
  595. if not user:
  596. return error_response(404, "用户不存在")
  597. price_list = price_list_crud.get(db, price_list_id)
  598. if not price_list:
  599. return error_response(404, "价格清单不存在")
  600. if price_list.supplier_id != user.id:
  601. return error_response(403, "无权查看此价格清单")
  602. items = price_list_item_crud.get_by_price_list(db, price_list_id)
  603. # 获取关联信息
  604. region = region_crud.get(db, price_list.region_id)
  605. region_name = region.name if region else None
  606. item_details = []
  607. for item in items:
  608. product = product_crud.get(db, item.product_id)
  609. category_name = None
  610. if product and product.category_id:
  611. category = category_crud.get(db, product.category_id)
  612. category_name = category.name if category else None
  613. unit_name = None
  614. if item.unit_id:
  615. u = unit_crud.get(db, item.unit_id)
  616. unit_name = u.name if u else None
  617. item_details.append({
  618. "productId": item.product_id,
  619. "productName": product.name if product else None,
  620. "categoryName": category_name,
  621. "unitId": item.unit_id,
  622. "unitName": unit_name,
  623. "price": float(item.price) if item.price else 0,
  624. "imageUrl": product.image_url if product else None
  625. })
  626. return success_response({
  627. "id": price_list.id,
  628. "regionId": price_list.region_id,
  629. "regionName": region_name,
  630. "version": price_list.version,
  631. "status": price_list.status,
  632. "effectiveTime": str(price_list.effective_time) if price_list.effective_time else None,
  633. "publishedTime": str(price_list.published_time) if price_list.published_time else None,
  634. "remark": price_list.remark,
  635. "createAt": str(price_list.create_at) if price_list.create_at else None,
  636. "items": item_details
  637. })
  638. # ============ 配送任务管理 ============
  639. @router.get("/tasks", response_model=dict)
  640. def list_supplier_tasks(
  641. page: int = Query(1, ge=1, description="页码"),
  642. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  643. supplierPrepareStatus: Optional[str] = Query(
  644. None,
  645. description="供应商备货状态 supplier_prepare_status;逗号多选,如 stocking,prepared",
  646. ),
  647. status: Optional[str] = Query(
  648. None,
  649. description="兼容:任务主状态 status;未传 supplierPrepareStatus 时生效",
  650. ),
  651. keyword: Optional[str] = Query(None, description="检索:配送单号、订单号、联系人、电话"),
  652. db: Session = Depends(get_db),
  653. current_user: dict = Depends(require_supplier)
  654. ):
  655. """获取供应商的配送任务列表(含商品缩略列表;供货合计按价格清单还原供货单价后汇总)"""
  656. from sqlalchemy import and_
  657. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  658. from app.crud.order import order_crud, order_item_crud
  659. from app.crud.product import product_crud
  660. from app.crud.unit import unit_crud
  661. from app.models.models import BizPriceListItem
  662. user = user_crud.get(db, current_user.get("user_id"))
  663. if not user:
  664. return error_response(404, "用户不存在")
  665. tasks, total = delivery_task_crud.get_multi_with_filters(
  666. db,
  667. supplier_id=user.id,
  668. supplier_prepare_status=supplierPrepareStatus,
  669. status=None if supplierPrepareStatus else status,
  670. keyword=keyword,
  671. skip=(page - 1) * pageSize,
  672. limit=pageSize
  673. )
  674. result = []
  675. for task in tasks:
  676. region_name = None
  677. if task.region_id:
  678. region = region_crud.get(db, task.region_id)
  679. region_name = region.name if region else None
  680. # 任务行 + 供货小计(与任务详情中价格清单逻辑一致)
  681. task_items = delivery_task_item_crud.get_by_task(db, task.id)
  682. total_items = len(task_items)
  683. delivered_items = len([i for i in task_items if i.status == "delivered"])
  684. order = order_crud.get(db, task.order_id) if task.order_id else None
  685. line_items = []
  686. total_quantity = 0.0
  687. supplier_quote_total = 0.0
  688. for ti in task_items:
  689. product = product_crud.get(db, ti.product_id)
  690. unit_name = None
  691. supplier_quote_line = None
  692. oi = None
  693. if ti.order_item_id:
  694. oi = order_item_crud.get(db, ti.order_item_id)
  695. if oi and oi.unit_id:
  696. u = unit_crud.get(db, oi.unit_id)
  697. unit_name = u.name if u else None
  698. if order and oi:
  699. pli = db.query(BizPriceListItem).filter(
  700. and_(
  701. BizPriceListItem.price_list_id == order.price_list_id,
  702. BizPriceListItem.product_id == oi.product_id,
  703. BizPriceListItem.unit_id == oi.unit_id,
  704. BizPriceListItem.is_del == 0,
  705. )
  706. ).first()
  707. if pli is not None and pli.price is not None:
  708. unit_price = _supplier_quote_unit_yuan_from_pli(pli)
  709. if unit_price is not None:
  710. qty = float(ti.quantity) if ti.quantity else 0.0
  711. supplier_quote_line = round(qty * unit_price, 2)
  712. supplier_quote_total += supplier_quote_line
  713. qty = float(ti.quantity) if ti.quantity else 0.0
  714. total_quantity += qty
  715. line_items.append({
  716. "id": ti.id,
  717. "productId": ti.product_id,
  718. "productName": product.name if product else None,
  719. "quantity": qty,
  720. "unitName": unit_name,
  721. "imageUrl": product.image_url if product else None,
  722. })
  723. # 获取配送员信息(列表不返回客户信息,仅展示配送员对接用)
  724. deliver_name = None
  725. deliver_phone = None
  726. if task.deliver_id:
  727. deliver = user_crud.get(db, task.deliver_id)
  728. if deliver:
  729. deliver_name = deliver.real_name
  730. deliver_phone = deliver.phone
  731. result.append({
  732. "id": task.id,
  733. "taskNo": task.task_no,
  734. "orderId": task.order_id,
  735. "regionId": task.region_id,
  736. "regionName": region_name,
  737. "supplierId": task.supplier_id,
  738. "deliverId": task.deliver_id,
  739. "deliverName": deliver_name,
  740. "deliverPhone": deliver_phone,
  741. "status": task.status,
  742. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  743. "effectiveTime": str(task.effective_time) if task.effective_time else None,
  744. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  745. "pickupRemark": task.pickup_remark,
  746. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  747. "totalItems": total_items,
  748. "deliveredItems": delivered_items,
  749. "totalQuantity": round(total_quantity, 3),
  750. "supplierQuoteTotal": round(supplier_quote_total, 2),
  751. "items": line_items,
  752. "createAt": str(task.create_at) if task.create_at else None
  753. })
  754. return success_response({
  755. "list": result,
  756. "total": total,
  757. "page": page,
  758. "pageSize": pageSize,
  759. "totalPages": (total + pageSize - 1) // pageSize
  760. })
  761. @router.get("/tasks/{task_id}", response_model=dict)
  762. def get_supplier_task_detail(
  763. task_id: str,
  764. db: Session = Depends(get_db),
  765. current_user: dict = Depends(require_supplier)
  766. ):
  767. """获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供货价)"""
  768. from sqlalchemy import and_
  769. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  770. from app.crud.order import order_crud, order_item_crud
  771. from app.crud.product import product_crud
  772. from app.crud.unit import unit_crud
  773. from app.models.models import BizPriceListItem
  774. user = user_crud.get(db, current_user.get("user_id"))
  775. if not user:
  776. return error_response(404, "用户不存在")
  777. task = delivery_task_crud.get(db, task_id)
  778. if not task:
  779. return error_response(404, "任务不存在")
  780. if task.supplier_id != user.id:
  781. return error_response(403, "无权查看此任务")
  782. order = None
  783. order_info = None
  784. if task.order_id:
  785. order = order_crud.get(db, task.order_id)
  786. if order:
  787. # 供应商备货详情仅展示客户订单号,不下发客户联系人、地址等
  788. order_info = {
  789. "orderNo": order.order_no,
  790. }
  791. deliver_info = None
  792. if task.deliver_id:
  793. deliver = user_crud.get(db, task.deliver_id)
  794. if deliver:
  795. deliver_info = {
  796. "deliverId": deliver.id,
  797. "deliverName": deliver.real_name,
  798. "phone": deliver.phone
  799. }
  800. task_items = delivery_task_item_crud.get_by_task(db, task_id)
  801. item_details = []
  802. for item in task_items:
  803. product = product_crud.get(db, item.product_id)
  804. unit_name = None
  805. supplier_quote_unit = None
  806. supplier_quote_line = None
  807. product_image_url = None
  808. product_measure_value = None
  809. product_unit_name = None
  810. if product:
  811. product_image_url = product.image_url
  812. if product.measure_value is not None:
  813. product_measure_value = float(product.measure_value)
  814. if product.unit_id:
  815. pu = unit_crud.get(db, product.unit_id)
  816. product_unit_name = pu.name if pu else None
  817. oi = None
  818. if item.order_item_id:
  819. oi = order_item_crud.get(db, item.order_item_id)
  820. if oi and oi.unit_id:
  821. u = unit_crud.get(db, oi.unit_id)
  822. unit_name = u.name if u else None
  823. if order and oi:
  824. pli = db.query(BizPriceListItem).filter(
  825. and_(
  826. BizPriceListItem.price_list_id == order.price_list_id,
  827. BizPriceListItem.product_id == oi.product_id,
  828. BizPriceListItem.unit_id == oi.unit_id,
  829. BizPriceListItem.is_del == 0,
  830. )
  831. ).first()
  832. if pli is not None and pli.price is not None:
  833. supplier_quote_unit = _supplier_quote_unit_yuan_from_pli(pli)
  834. if supplier_quote_unit is not None:
  835. qty = float(item.quantity) if item.quantity else 0.0
  836. supplier_quote_line = round(qty * supplier_quote_unit, 2)
  837. item_details.append({
  838. "id": item.id,
  839. "orderItemId": item.order_item_id,
  840. "productId": item.product_id,
  841. "productName": product.name if product else None,
  842. "imageUrl": product_image_url,
  843. "measureValue": product_measure_value,
  844. "productUnitName": product_unit_name,
  845. "unitName": unit_name,
  846. "quantity": float(item.quantity) if item.quantity else 0,
  847. "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
  848. "supplierQuoteUnit": supplier_quote_unit,
  849. "supplierQuoteLine": supplier_quote_line,
  850. "status": item.status,
  851. "deliveryTime": str(item.delivery_time) if item.delivery_time else None
  852. })
  853. prepared_images = [
  854. a.file_url
  855. for a in attachment_crud.get_by_business(db, "delivery_prepare", task_id)
  856. if getattr(a, "file_url", None)
  857. ]
  858. if (not prepared_images) and getattr(task, "pickup_image_url", None):
  859. prepared_images = [s.strip() for s in str(task.pickup_image_url).split(",") if s and str(s).strip()]
  860. return success_response({
  861. "id": task.id,
  862. "taskNo": task.task_no,
  863. "orderId": task.order_id,
  864. "regionId": task.region_id,
  865. "status": task.status,
  866. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  867. "effectiveTime": str(task.effective_time) if task.effective_time else None,
  868. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  869. "pickupRemark": task.pickup_remark,
  870. "pickupImageUrl": task.pickup_image_url,
  871. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  872. "deliveryRemark": task.delivery_remark,
  873. "deliveryImageUrl": task.delivery_image_url,
  874. "preparedImages": prepared_images,
  875. "order": order_info,
  876. "deliver": deliver_info,
  877. "items": item_details,
  878. "createAt": str(task.create_at) if task.create_at else None
  879. })
  880. def _notify_deliver_stock_ready(db, task, user, remark: str = None) -> bool:
  881. """备货完成后通知配送员(微信订阅消息,失败不阻断主流程)"""
  882. if not task.deliver_id:
  883. return False
  884. try:
  885. from app.core.wechat_message import WechatMessageService
  886. deliver = user_crud.get(db, task.deliver_id)
  887. if deliver and hasattr(deliver, "openid"):
  888. WechatMessageService.send_delivery_notice(
  889. openid=deliver.openid,
  890. task_no=task.task_no,
  891. supplier_name=user.real_name or "供应商",
  892. address="商品已备好,请尽快取货",
  893. remark=remark or "供应商已备货完成,请及时取货",
  894. )
  895. return True
  896. except Exception:
  897. pass
  898. return False
  899. @router.post("/tasks/{task_id}/start-stock", response_model=dict)
  900. def supplier_start_stock(
  901. task_id: str,
  902. request: dict,
  903. db: Session = Depends(get_db),
  904. current_user: dict = Depends(require_supplier),
  905. ):
  906. """供应商开始备货:assigned -> supplier_stocking,并写入订单状态日志(配送进度)"""
  907. from app.crud.delivery import delivery_task_crud
  908. user = user_crud.get(db, current_user.get("user_id"))
  909. if not user:
  910. return error_response(404, "用户不存在")
  911. task = delivery_task_crud.get(db, task_id)
  912. if not task:
  913. return error_response(404, "任务不存在")
  914. if task.supplier_id != user.id:
  915. return error_response(403, "无权操作此任务")
  916. if (getattr(task, "supplier_prepare_status", None) or "pending_prepare") != "pending_prepare":
  917. return error_response(400, "当前状态不可开始备货(需为待备货/已分配)")
  918. remark = (request.get("remark") or "").strip() or None
  919. task = delivery_task_crud.update_status(
  920. db,
  921. task_id=task_id,
  922. new_status="supplier_stocking",
  923. operator_id=user.id,
  924. remark=remark,
  925. operator_real_name=user.real_name,
  926. operator_role="supplier",
  927. )
  928. if not task:
  929. return error_response(400, "操作失败")
  930. return success_response({
  931. "id": task.id,
  932. "taskNo": task.task_no,
  933. "status": task.status,
  934. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  935. "delivererStatus": getattr(task, "deliverer_status", None),
  936. "message": "已开始备货",
  937. })
  938. @router.post("/tasks/{task_id}/finish-stock", response_model=dict)
  939. def supplier_finish_stock(
  940. task_id: str,
  941. request: dict,
  942. db: Session = Depends(get_db),
  943. current_user: dict = Depends(require_supplier),
  944. ):
  945. """供应商备货完成:supplier_stocking -> supplier_ready(待配送员取货),写入订单日志并可通知配送员"""
  946. from app.crud.delivery import delivery_task_crud
  947. user = user_crud.get(db, current_user.get("user_id"))
  948. if not user:
  949. return error_response(404, "用户不存在")
  950. task = delivery_task_crud.get(db, task_id)
  951. if not task:
  952. return error_response(404, "任务不存在")
  953. if task.supplier_id != user.id:
  954. return error_response(403, "无权操作此任务")
  955. ss = getattr(task, "supplier_prepare_status", None) or "pending_prepare"
  956. if ss == "pending_prepare":
  957. return error_response(400, "请先开始备货后再确认备货完成")
  958. if ss not in ("stocking", "prepared"):
  959. return error_response(400, "当前状态不可确认备货完成")
  960. if ss == "prepared":
  961. ds = (getattr(task, "deliverer_status", None) or "").strip().lower()
  962. if ds != "pending_pickup":
  963. return error_response(400, "配送员已取货,备货凭证不可再修改")
  964. remark = (request.get("remark") or request.get("preparedRemark") or "").strip() or None
  965. image_urls = _normalize_image_urls(request.get("imageUrls"))
  966. if not image_urls:
  967. prep_img = request.get("preparedImageUrl") or request.get("pickupImageUrl")
  968. image_urls = _normalize_image_urls(prep_img)
  969. if not image_urls:
  970. return error_response(400, "请上传备货完成凭证照片")
  971. if ss == "stocking":
  972. task = delivery_task_crud.update_status(
  973. db,
  974. task_id=task_id,
  975. new_status="supplier_ready",
  976. operator_id=user.id,
  977. remark=remark,
  978. image_urls=image_urls,
  979. operator_real_name=user.real_name,
  980. operator_role="supplier",
  981. )
  982. if not task:
  983. return error_response(400, "操作失败")
  984. else:
  985. task = delivery_task_crud.get(db, task_id)
  986. if not task:
  987. return error_response(404, "任务不存在")
  988. _replace_delivery_prepare_attachments(
  989. db,
  990. task_id=task_id,
  991. image_urls=image_urls,
  992. uploader_id=user.id,
  993. uploader_name=user.real_name,
  994. )
  995. db.commit()
  996. deliver_notified = _notify_deliver_stock_ready(db, task, user, remark) if ss == "stocking" else False
  997. return success_response({
  998. "id": task.id,
  999. "taskNo": task.task_no,
  1000. "status": task.status,
  1001. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  1002. "delivererStatus": getattr(task, "deliverer_status", None),
  1003. "message": ("已确认备货完成" if ss == "stocking" else "备货凭证已更新")
  1004. + (",已通知配送员" if deliver_notified else ""),
  1005. })
  1006. # 兼容旧路径:与 start-stock / finish-stock 行为一致
  1007. @router.post("/tasks/{task_id}/confirm-pickup", response_model=dict)
  1008. def supplier_confirm_pickup_deprecated(
  1009. task_id: str,
  1010. request: dict,
  1011. db: Session = Depends(get_db),
  1012. current_user: dict = Depends(require_supplier),
  1013. ):
  1014. """[兼容] 请改用 POST /tasks/{id}/start-stock"""
  1015. return supplier_start_stock(task_id, request, db, current_user)
  1016. @router.post("/tasks/{task_id}/confirm-prepared", response_model=dict)
  1017. def supplier_confirm_prepared_deprecated(
  1018. task_id: str,
  1019. request: dict,
  1020. db: Session = Depends(get_db),
  1021. current_user: dict = Depends(require_supplier),
  1022. ):
  1023. """[兼容] 请改用 POST /tasks/{id}/finish-stock"""
  1024. return supplier_finish_stock(task_id, request, db, current_user)
  1025. @router.get("/dashboard/statistics", response_model=dict)
  1026. def supplier_dashboard_statistics(
  1027. db: Session = Depends(get_db),
  1028. current_user: dict = Depends(require_supplier)
  1029. ):
  1030. """供应商首页统计(询价/报价/任务概览)"""
  1031. from app.crud.delivery import delivery_task_crud
  1032. from app.models.models import BizInquiry, BizQuotation
  1033. from sqlalchemy import and_
  1034. uid = current_user.get("user_id")
  1035. user = user_crud.get(db, uid)
  1036. rid = user.region_id if user else None
  1037. q_inq = db.query(BizInquiry).filter(
  1038. and_(BizInquiry.is_del == 0, BizInquiry.status == "published")
  1039. )
  1040. if rid:
  1041. q_inq = q_inq.filter(BizInquiry.region_id == rid)
  1042. pending_inq = q_inq.count()
  1043. my_quo = db.query(BizQuotation).filter(
  1044. and_(BizQuotation.is_del == 0, BizQuotation.supplier_id == uid)
  1045. ).count()
  1046. pending_tasks, _ = delivery_task_crud.get_by_supplier(
  1047. db, supplier_id=uid, status="assigned", skip=0, limit=500
  1048. )
  1049. stocking, _ = delivery_task_crud.get_by_supplier(
  1050. db, supplier_id=uid, status="supplier_stocking", skip=0, limit=500
  1051. )
  1052. return success_response({
  1053. "pendingInquiries": pending_inq,
  1054. "myQuotations": my_quo,
  1055. "pendingTasks": len(pending_tasks) + len(stocking),
  1056. "todayDeliveries": 0,
  1057. })
  1058. @router.get("/products/{product_id}", response_model=dict)
  1059. def get_supplier_product_detail(
  1060. product_id: str,
  1061. db: Session = Depends(get_db),
  1062. current_user: dict = Depends(require_supplier)
  1063. ):
  1064. """供应商端商品详情:轮播、基础信息、参数、富文本、详情媒体"""
  1065. from app.crud.product import product_crud
  1066. from app.crud.unit import unit_crud
  1067. from app.crud.product_banner import product_banner
  1068. from app.crud.product_parameter_value import product_parameter_value
  1069. from app.crud.product_detail_media import product_detail_media
  1070. product = product_crud.get(db, product_id)
  1071. if not product or getattr(product, "is_del", 0) == 1:
  1072. return error_response(404, "商品不存在")
  1073. unit = unit_crud.get(db, product.unit_id) if product.unit_id else None
  1074. banners_raw = product_banner.get_by_product(db, product_id)
  1075. banners = [{
  1076. "mediaType": b.media_type,
  1077. "fileUrl": b.file_url,
  1078. "thumbnailUrl": b.thumbnail_url,
  1079. "sortOrder": b.sort_order,
  1080. } for b in banners_raw]
  1081. pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id)
  1082. parameter_values = []
  1083. for row in pv_rows:
  1084. val = row.get("param_value")
  1085. if val is None or str(val).strip() == "":
  1086. continue
  1087. parameter_values.append({
  1088. "parameterId": row.get("parameter_id"),
  1089. "parameterName": row.get("parameter_name") or "",
  1090. "paramValue": str(val),
  1091. })
  1092. media_raw = product_detail_media.get_by_product(db, product_id)
  1093. detail_media = []
  1094. for m in media_raw:
  1095. if m.content_type not in ("image", "video"):
  1096. continue
  1097. if not m.file_url:
  1098. continue
  1099. detail_media.append({
  1100. "id": m.id,
  1101. "contentType": m.content_type,
  1102. "fileUrl": m.file_url,
  1103. "thumbnailUrl": m.thumbnail_url,
  1104. "description": m.description,
  1105. "sortOrder": m.sort_order,
  1106. })
  1107. return success_response({
  1108. "product": {
  1109. "id": product.id,
  1110. "name": product.name,
  1111. "unitId": product.unit_id,
  1112. "unitName": unit.name if unit else None,
  1113. "measureValue": float(product.measure_value) if product.measure_value is not None else None,
  1114. "detailContent": product.detail_content or "",
  1115. },
  1116. "banners": banners,
  1117. "parameterValues": parameter_values,
  1118. "detailMedia": detail_media,
  1119. })