views.py 96 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225
  1. from rest_framework.views import APIView
  2. from rest_framework.response import Response
  3. from rest_framework import status
  4. from rest_framework.parsers import MultiPartParser, FormParser
  5. from django.db import transaction
  6. import pandas as pd
  7. from .models import Product, ProductType, ProductAttribute, AttributePossibleValue
  8. from .serializers import (
  9. SingleProductRequestSerializer,
  10. BatchProductRequestSerializer,
  11. ProductAttributeResultSerializer,
  12. BatchProductResponseSerializer,
  13. ProductSerializer,
  14. ProductTypeSerializer,
  15. ProductAttributeSerializer,
  16. AttributePossibleValueSerializer
  17. )
  18. from .services import ProductAttributeService
  19. from .ocr_service import OCRService
  20. # Sample test images (publicly available)
  21. SAMPLE_IMAGES = {
  22. "tshirt": "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab",
  23. "dress": "https://images.unsplash.com/photo-1595777457583-95e059d581b8",
  24. "jeans": "https://images.unsplash.com/photo-1542272604-787c3835535d"
  25. }
  26. # ==================== Updated views.py ====================
  27. from rest_framework.views import APIView
  28. from rest_framework.response import Response
  29. from rest_framework import status
  30. from .models import Product
  31. from .services import ProductAttributeService
  32. from .ocr_service import OCRService
  33. from .visual_processing_service import VisualProcessingService
  34. class ExtractProductAttributesView(APIView):
  35. """
  36. API endpoint to extract product attributes for a single product by item_id.
  37. Fetches product details from database with source tracking.
  38. Returns attributes in array format: [{"value": "...", "source": "..."}]
  39. Includes OCR and Visual Processing results.
  40. """
  41. def post(self, request):
  42. serializer = SingleProductRequestSerializer(data=request.data)
  43. if not serializer.is_valid():
  44. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  45. validated_data = serializer.validated_data
  46. item_id = validated_data.get("item_id")
  47. # Fetch product from DB
  48. try:
  49. product = Product.objects.get(item_id=item_id)
  50. except Product.DoesNotExist:
  51. return Response(
  52. {"error": f"Product with item_id '{item_id}' not found."},
  53. status=status.HTTP_404_NOT_FOUND
  54. )
  55. # Extract product details
  56. title = product.product_name
  57. short_desc = product.product_short_description
  58. long_desc = product.product_long_description
  59. image_url = product.image_path
  60. # Process image for OCR if required
  61. ocr_results = None
  62. ocr_text = None
  63. visual_results = None
  64. if validated_data.get("process_image", True) and image_url:
  65. # OCR Processing
  66. ocr_service = OCRService()
  67. ocr_results = ocr_service.process_image(image_url)
  68. if ocr_results and ocr_results.get("detected_text"):
  69. ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  70. ocr_results, validated_data.get("model")
  71. )
  72. ocr_results["extracted_attributes"] = ocr_attrs
  73. ocr_text = "\n".join([
  74. f"{item['text']} (confidence: {item['confidence']:.2f})"
  75. for item in ocr_results["detected_text"]
  76. ])
  77. # Visual Processing
  78. visual_service = VisualProcessingService()
  79. product_type_hint = product.product_type if hasattr(product, 'product_type') else None
  80. visual_results = visual_service.process_image(image_url, product_type_hint)
  81. # Combine all product text with source tracking
  82. product_text, source_map = ProductAttributeService.combine_product_text(
  83. title=title,
  84. short_desc=short_desc,
  85. long_desc=long_desc,
  86. ocr_text=ocr_text
  87. )
  88. # Extract attributes with enhanced features and source tracking
  89. result = ProductAttributeService.extract_attributes(
  90. product_text=product_text,
  91. mandatory_attrs=validated_data["mandatory_attrs"],
  92. source_map=source_map,
  93. model=validated_data.get("model"),
  94. extract_additional=validated_data.get("extract_additional", True),
  95. multiple=validated_data.get("multiple", []),
  96. threshold_abs=validated_data.get("threshold_abs", 0.65),
  97. margin=validated_data.get("margin", 0.15),
  98. use_dynamic_thresholds=validated_data.get("use_dynamic_thresholds", True),
  99. use_adaptive_margin=validated_data.get("use_adaptive_margin", True),
  100. use_semantic_clustering=validated_data.get("use_semantic_clustering", True)
  101. )
  102. # Attach OCR results if available
  103. if ocr_results:
  104. result["ocr_results"] = ocr_results
  105. # Attach Visual Processing results if available
  106. if visual_results:
  107. result["visual_results"] = visual_results
  108. response_serializer = ProductAttributeResultSerializer(data=result)
  109. if response_serializer.is_valid():
  110. return Response(response_serializer.data, status=status.HTTP_200_OK)
  111. return Response(result, status=status.HTTP_200_OK)
  112. # class BatchExtractProductAttributesView(APIView):
  113. # """
  114. # API endpoint to extract product attributes for multiple products in batch.
  115. # Uses item-specific mandatory_attrs with source tracking.
  116. # Returns attributes in array format: [{"value": "...", "source": "..."}]
  117. # Includes OCR and Visual Processing results.
  118. # """
  119. # def post(self, request):
  120. # serializer = BatchProductRequestSerializer(data=request.data)
  121. # if not serializer.is_valid():
  122. # return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  123. # validated_data = serializer.validated_data
  124. # # DEBUG: Print what we received
  125. # print("\n" + "="*80)
  126. # print("BATCH REQUEST - RECEIVED DATA")
  127. # print("="*80)
  128. # print(f"Raw request data keys: {request.data.keys()}")
  129. # print(f"Multiple field in request: {request.data.get('multiple')}")
  130. # print(f"Validated multiple field: {validated_data.get('multiple')}")
  131. # print("="*80 + "\n")
  132. # # Get batch-level settings
  133. # product_list = validated_data.get("products", [])
  134. # model = validated_data.get("model")
  135. # extract_additional = validated_data.get("extract_additional", True)
  136. # process_image = validated_data.get("process_image", True)
  137. # multiple = validated_data.get("multiple", [])
  138. # threshold_abs = validated_data.get("threshold_abs", 0.65)
  139. # margin = validated_data.get("margin", 0.15)
  140. # use_dynamic_thresholds = validated_data.get("use_dynamic_thresholds", True)
  141. # use_adaptive_margin = validated_data.get("use_adaptive_margin", True)
  142. # use_semantic_clustering = validated_data.get("use_semantic_clustering", True)
  143. # # DEBUG: Print extracted settings
  144. # print(f"Extracted multiple parameter: {multiple}")
  145. # print(f"Type: {type(multiple)}")
  146. # # Extract all item_ids to query the database efficiently
  147. # item_ids = [p['item_id'] for p in product_list]
  148. # # Fetch all products in one query
  149. # products_queryset = Product.objects.filter(item_id__in=item_ids)
  150. # # Create a dictionary for easy lookup: item_id -> Product object
  151. # product_map = {product.item_id: product for product in products_queryset}
  152. # found_ids = set(product_map.keys())
  153. # results = []
  154. # successful = 0
  155. # failed = 0
  156. # for product_entry in product_list:
  157. # item_id = product_entry['item_id']
  158. # # Get item-specific mandatory attributes
  159. # mandatory_attrs = product_entry['mandatory_attrs']
  160. # if item_id not in found_ids:
  161. # failed += 1
  162. # results.append({
  163. # "product_id": item_id,
  164. # "error": "Product not found in database"
  165. # })
  166. # continue
  167. # product = product_map[item_id]
  168. # try:
  169. # title = product.product_name
  170. # short_desc = product.product_short_description
  171. # long_desc = product.product_long_description
  172. # image_url = product.image_path
  173. # # image_url = "https://images.unsplash.com/photo-1595777457583-95e059d581b8"
  174. # ocr_results = None
  175. # ocr_text = None
  176. # visual_results = None
  177. # # Image Processing Logic
  178. # if process_image and image_url:
  179. # # OCR Processing
  180. # ocr_service = OCRService()
  181. # ocr_results = ocr_service.process_image(image_url)
  182. # print(f"OCR results for {item_id}: {ocr_results}")
  183. # if ocr_results and ocr_results.get("detected_text"):
  184. # ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  185. # ocr_results, model
  186. # )
  187. # ocr_results["extracted_attributes"] = ocr_attrs
  188. # ocr_text = "\n".join([
  189. # f"{item['text']} (confidence: {item['confidence']:.2f})"
  190. # for item in ocr_results["detected_text"]
  191. # ])
  192. # # Visual Processing
  193. # visual_service = VisualProcessingService()
  194. # product_type_hint = product.product_type if hasattr(product, 'product_type') else None
  195. # visual_results = visual_service.process_image(image_url, product_type_hint)
  196. # print(f"Visual results for {item_id}: {visual_results.get('visual_attributes', {})}")
  197. # # Format visual attributes to array format with source tracking
  198. # if visual_results and visual_results.get('visual_attributes'):
  199. # visual_results['visual_attributes'] = ProductAttributeService.format_visual_attributes(
  200. # visual_results['visual_attributes']
  201. # )
  202. # # Combine product text with source tracking
  203. # product_text, source_map = ProductAttributeService.combine_product_text(
  204. # title=title,
  205. # short_desc=short_desc,
  206. # long_desc=long_desc,
  207. # ocr_text=ocr_text
  208. # )
  209. # # DEBUG: Print before extraction
  210. # print(f"\n>>> Extracting for product {item_id}")
  211. # print(f" Passing multiple: {multiple}")
  212. # # Attribute Extraction with source tracking (returns array format)
  213. # extracted = ProductAttributeService.extract_attributes(
  214. # product_text=product_text,
  215. # mandatory_attrs=mandatory_attrs,
  216. # source_map=source_map,
  217. # model=model,
  218. # extract_additional=extract_additional,
  219. # multiple=multiple,
  220. # threshold_abs=threshold_abs,
  221. # margin=margin,
  222. # use_dynamic_thresholds=use_dynamic_thresholds,
  223. # use_adaptive_margin=use_adaptive_margin,
  224. # use_semantic_clustering=use_semantic_clustering
  225. # )
  226. # result = {
  227. # "product_id": product.item_id,
  228. # "mandatory": extracted.get("mandatory", {}),
  229. # "additional": extracted.get("additional", {}),
  230. # }
  231. # # Attach OCR results if available
  232. # if ocr_results:
  233. # result["ocr_results"] = ocr_results
  234. # # Attach Visual Processing results if available
  235. # if visual_results:
  236. # result["visual_results"] = visual_results
  237. # results.append(result)
  238. # successful += 1
  239. # except Exception as e:
  240. # failed += 1
  241. # results.append({
  242. # "product_id": item_id,
  243. # "error": str(e)
  244. # })
  245. # batch_result = {
  246. # "results": results,
  247. # "total_products": len(product_list),
  248. # "successful": successful,
  249. # "failed": failed
  250. # }
  251. # response_serializer = BatchProductResponseSerializer(data=batch_result)
  252. # if response_serializer.is_valid():
  253. # return Response(response_serializer.data, status=status.HTTP_200_OK)
  254. # return Response(batch_result, status=status.HTTP_200_OK)
  255. # Replace the BatchExtractProductAttributesView in your views.py with this updated version
  256. class BatchExtractProductAttributesView(APIView):
  257. """
  258. API endpoint to extract product attributes for multiple products in batch.
  259. Uses item-specific mandatory_attrs with source tracking.
  260. Returns attributes in array format with original_value field.
  261. Includes OCR and Visual Processing results.
  262. """
  263. def post(self, request):
  264. serializer = BatchProductRequestSerializer(data=request.data)
  265. if not serializer.is_valid():
  266. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  267. validated_data = serializer.validated_data
  268. # Get batch-level settings
  269. product_list = validated_data.get("products", [])
  270. model = validated_data.get("model")
  271. extract_additional = validated_data.get("extract_additional", True)
  272. process_image = validated_data.get("process_image", True)
  273. multiple = validated_data.get("multiple", [])
  274. threshold_abs = validated_data.get("threshold_abs", 0.65)
  275. margin = validated_data.get("margin", 0.15)
  276. use_dynamic_thresholds = validated_data.get("use_dynamic_thresholds", True)
  277. use_adaptive_margin = validated_data.get("use_adaptive_margin", True)
  278. use_semantic_clustering = validated_data.get("use_semantic_clustering", True)
  279. # Extract all item_ids to query the database efficiently
  280. item_ids = [p['item_id'] for p in product_list]
  281. # Fetch all products in one query
  282. products_queryset = Product.objects.filter(item_id__in=item_ids)
  283. product_map = {product.item_id: product for product in products_queryset}
  284. found_ids = set(product_map.keys())
  285. # Fetch all original attribute values for these products in one query
  286. original_values_qs = ProductAttributeValue.objects.filter(
  287. product__item_id__in=item_ids
  288. ).select_related('product')
  289. # Create a nested dictionary: {item_id: {attribute_name: original_value}}
  290. original_values_map = {}
  291. for attr_val in original_values_qs:
  292. item_id = attr_val.product.item_id
  293. if item_id not in original_values_map:
  294. original_values_map[item_id] = {}
  295. original_values_map[item_id][attr_val.attribute_name] = attr_val.original_value
  296. results = []
  297. successful = 0
  298. failed = 0
  299. for product_entry in product_list:
  300. item_id = product_entry['item_id']
  301. mandatory_attrs = product_entry['mandatory_attrs']
  302. if item_id not in found_ids:
  303. failed += 1
  304. results.append({
  305. "product_id": item_id,
  306. "error": "Product not found in database"
  307. })
  308. continue
  309. product = product_map[item_id]
  310. try:
  311. title = product.product_name
  312. short_desc = product.product_short_description
  313. long_desc = product.product_long_description
  314. image_url = product.image_path
  315. ocr_results = None
  316. ocr_text = None
  317. visual_results = None
  318. # Image Processing Logic
  319. if process_image and image_url:
  320. # OCR Processing
  321. ocr_service = OCRService()
  322. ocr_results = ocr_service.process_image(image_url)
  323. if ocr_results and ocr_results.get("detected_text"):
  324. ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  325. ocr_results, model
  326. )
  327. ocr_results["extracted_attributes"] = ocr_attrs
  328. ocr_text = "\n".join([
  329. f"{item['text']} (confidence: {item['confidence']:.2f})"
  330. for item in ocr_results["detected_text"]
  331. ])
  332. # Visual Processing
  333. visual_service = VisualProcessingService()
  334. product_type_hint = product.product_type if hasattr(product, 'product_type') else None
  335. visual_results = visual_service.process_image(image_url, product_type_hint)
  336. # Format visual attributes to array format with source tracking
  337. if visual_results and visual_results.get('visual_attributes'):
  338. visual_results['visual_attributes'] = ProductAttributeService.format_visual_attributes(
  339. visual_results['visual_attributes']
  340. )
  341. # Combine product text with source tracking
  342. product_text, source_map = ProductAttributeService.combine_product_text(
  343. title=title,
  344. short_desc=short_desc,
  345. long_desc=long_desc,
  346. ocr_text=ocr_text
  347. )
  348. # Attribute Extraction with source tracking (returns array format)
  349. extracted = ProductAttributeService.extract_attributes(
  350. product_text=product_text,
  351. mandatory_attrs=mandatory_attrs,
  352. source_map=source_map,
  353. model=model,
  354. extract_additional=extract_additional,
  355. multiple=multiple,
  356. threshold_abs=threshold_abs,
  357. margin=margin,
  358. use_dynamic_thresholds=use_dynamic_thresholds,
  359. use_adaptive_margin=use_adaptive_margin,
  360. use_semantic_clustering=use_semantic_clustering
  361. )
  362. # Add original_value to each extracted attribute
  363. original_attrs = original_values_map.get(item_id, {})
  364. # Process mandatory attributes
  365. for attr_name, attr_values in extracted.get("mandatory", {}).items():
  366. if isinstance(attr_values, list):
  367. for attr_obj in attr_values:
  368. if isinstance(attr_obj, dict):
  369. # Add original_value if it exists
  370. attr_obj["original_value"] = original_attrs.get(attr_name, "")
  371. # Process additional attributes
  372. for attr_name, attr_values in extracted.get("additional", {}).items():
  373. if isinstance(attr_values, list):
  374. for attr_obj in attr_values:
  375. if isinstance(attr_obj, dict):
  376. # Add original_value if it exists
  377. attr_obj["original_value"] = original_attrs.get(attr_name, "")
  378. result = {
  379. "product_id": product.item_id,
  380. "mandatory": extracted.get("mandatory", {}),
  381. "additional": extracted.get("additional", {}),
  382. }
  383. # Attach OCR results if available
  384. if ocr_results:
  385. result["ocr_results"] = ocr_results
  386. # Attach Visual Processing results if available
  387. if visual_results:
  388. result["visual_results"] = visual_results
  389. results.append(result)
  390. successful += 1
  391. except Exception as e:
  392. failed += 1
  393. results.append({
  394. "product_id": item_id,
  395. "error": str(e)
  396. })
  397. batch_result = {
  398. "results": results,
  399. "total_products": len(product_list),
  400. "successful": successful,
  401. "failed": failed
  402. }
  403. response_serializer = BatchProductResponseSerializer(data=batch_result)
  404. if response_serializer.is_valid():
  405. return Response(response_serializer.data, status=status.HTTP_200_OK)
  406. return Response(batch_result, status=status.HTTP_200_OK)
  407. class ProductListView(APIView):
  408. """
  409. GET API to list all products with details
  410. """
  411. def get(self, request):
  412. products = Product.objects.all()
  413. serializer = ProductSerializer(products, many=True)
  414. return Response(serializer.data, status=status.HTTP_200_OK)
  415. from rest_framework.views import APIView
  416. from rest_framework.response import Response
  417. from rest_framework import status
  418. from rest_framework.parsers import MultiPartParser, FormParser
  419. import pandas as pd
  420. from .models import Product
  421. # class ProductUploadExcelView(APIView):
  422. # """
  423. # POST API to upload an Excel file and add/update data in Product model.
  424. # - Creates new records if they don't exist.
  425. # - Updates existing ones (e.g., when image_path or other fields change).
  426. # """
  427. # parser_classes = (MultiPartParser, FormParser)
  428. # def post(self, request, *args, **kwargs):
  429. # file_obj = request.FILES.get('file')
  430. # if not file_obj:
  431. # return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
  432. # try:
  433. # # Read Excel into DataFrame
  434. # df = pd.read_excel(file_obj)
  435. # df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
  436. # expected_cols = {
  437. # 'item_id',
  438. # 'product_name',
  439. # 'product_long_description',
  440. # 'product_short_description',
  441. # 'product_type',
  442. # 'image_path'
  443. # }
  444. # # Check required columns
  445. # if not expected_cols.issubset(df.columns):
  446. # return Response({
  447. # 'error': 'Missing required columns',
  448. # 'required_columns': list(expected_cols)
  449. # }, status=status.HTTP_400_BAD_REQUEST)
  450. # created_count = 0
  451. # updated_count = 0
  452. # # Loop through rows and update or create
  453. # for _, row in df.iterrows():
  454. # item_id = str(row.get('item_id', '')).strip()
  455. # if not item_id:
  456. # continue # Skip rows without an item_id
  457. # defaults = {
  458. # 'product_name': row.get('product_name', ''),
  459. # 'product_long_description': row.get('product_long_description', ''),
  460. # 'product_short_description': row.get('product_short_description', ''),
  461. # 'product_type': row.get('product_type', ''),
  462. # 'image_path': row.get('image_path', ''),
  463. # }
  464. # obj, created = Product.objects.update_or_create(
  465. # item_id=item_id,
  466. # defaults=defaults
  467. # )
  468. # if created:
  469. # created_count += 1
  470. # else:
  471. # updated_count += 1
  472. # return Response({
  473. # 'message': f'Upload successful.',
  474. # 'created': f'{created_count} new records added.',
  475. # 'updated': f'{updated_count} existing records updated.'
  476. # }, status=status.HTTP_201_CREATED)
  477. # except Exception as e:
  478. # return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  479. # Replace the ProductUploadExcelView in your views.py with this updated version
  480. from rest_framework.views import APIView
  481. from rest_framework.response import Response
  482. from rest_framework import status
  483. from rest_framework.parsers import MultiPartParser, FormParser
  484. from django.db import transaction
  485. import pandas as pd
  486. from .models import Product, ProductAttributeValue
  487. # class ProductUploadExcelView(APIView):
  488. # """
  489. # POST API to upload an Excel file with two sheets:
  490. # 1. 'Products' sheet - Product details
  491. # 2. 'Attribute_values' sheet - Original attribute values
  492. # Creates/updates both products and their attribute values in a single transaction.
  493. # """
  494. # parser_classes = (MultiPartParser, FormParser)
  495. # def post(self, request, *args, **kwargs):
  496. # file_obj = request.FILES.get('file')
  497. # if not file_obj:
  498. # return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
  499. # try:
  500. # # Read all sheets from Excel file
  501. # excel_file = pd.ExcelFile(file_obj)
  502. # # Check if required sheets exist
  503. # if 'Products' not in excel_file.sheet_names:
  504. # return Response({
  505. # 'error': "Missing 'Products' sheet",
  506. # 'available_sheets': excel_file.sheet_names
  507. # }, status=status.HTTP_400_BAD_REQUEST)
  508. # # Read Products sheet
  509. # df_products = pd.read_excel(excel_file, sheet_name='Products')
  510. # df_products.columns = [c.strip().lower().replace(' ', '_') for c in df_products.columns]
  511. # # Check required columns for Products
  512. # expected_product_cols = {
  513. # 'item_id',
  514. # 'product_name',
  515. # 'product_long_description',
  516. # 'product_short_description',
  517. # 'product_type',
  518. # 'image_path'
  519. # }
  520. # if not expected_product_cols.issubset(df_products.columns):
  521. # return Response({
  522. # 'error': 'Missing required columns in Products sheet',
  523. # 'required_columns': list(expected_product_cols),
  524. # 'found_columns': list(df_products.columns)
  525. # }, status=status.HTTP_400_BAD_REQUEST)
  526. # # Read Attribute_values sheet if it exists
  527. # df_attributes = None
  528. # has_attributes_sheet = 'Attribute_values' in excel_file.sheet_names
  529. # if has_attributes_sheet:
  530. # df_attributes = pd.read_excel(excel_file, sheet_name='Attribute_values')
  531. # df_attributes.columns = [c.strip().lower().replace(' ', '_') for c in df_attributes.columns]
  532. # # Check required columns for Attribute_values
  533. # expected_attr_cols = {'item_id', 'attribute_name', 'original_value'}
  534. # if not expected_attr_cols.issubset(df_attributes.columns):
  535. # return Response({
  536. # 'error': 'Missing required columns in Attribute_values sheet',
  537. # 'required_columns': list(expected_attr_cols),
  538. # 'found_columns': list(df_attributes.columns)
  539. # }, status=status.HTTP_400_BAD_REQUEST)
  540. # # Initialize counters
  541. # products_created = 0
  542. # products_updated = 0
  543. # attributes_created = 0
  544. # attributes_updated = 0
  545. # products_failed = 0
  546. # attributes_failed = 0
  547. # errors = []
  548. # # Use transaction to ensure atomicity
  549. # with transaction.atomic():
  550. # # Process Products sheet
  551. # for idx, row in df_products.iterrows():
  552. # item_id = str(row.get('item_id', '')).strip()
  553. # if not item_id:
  554. # products_failed += 1
  555. # errors.append(f"Products Row {idx + 2}: Missing item_id")
  556. # continue
  557. # try:
  558. # defaults = {
  559. # 'product_name': str(row.get('product_name', '')),
  560. # 'product_long_description': str(row.get('product_long_description', '')),
  561. # 'product_short_description': str(row.get('product_short_description', '')),
  562. # 'product_type': str(row.get('product_type', '')),
  563. # 'image_path': str(row.get('image_path', '')),
  564. # }
  565. # obj, created = Product.objects.update_or_create(
  566. # item_id=item_id,
  567. # defaults=defaults
  568. # )
  569. # if created:
  570. # products_created += 1
  571. # else:
  572. # products_updated += 1
  573. # except Exception as e:
  574. # products_failed += 1
  575. # errors.append(f"Products Row {idx + 2} (item_id: {item_id}): {str(e)}")
  576. # # Process Attribute_values sheet if it exists
  577. # if has_attributes_sheet and df_attributes is not None:
  578. # # Group by item_id to optimize lookups
  579. # item_ids_in_attrs = df_attributes['item_id'].unique()
  580. # # Fetch all products at once
  581. # existing_products = {
  582. # p.item_id: p
  583. # for p in Product.objects.filter(item_id__in=item_ids_in_attrs)
  584. # }
  585. # for idx, row in df_attributes.iterrows():
  586. # item_id = str(row.get('item_id', '')).strip()
  587. # attribute_name = str(row.get('attribute_name', '')).strip()
  588. # original_value = str(row.get('original_value', '')).strip()
  589. # if not item_id or not attribute_name:
  590. # attributes_failed += 1
  591. # errors.append(
  592. # f"Attribute_values Row {idx + 2}: Missing item_id or attribute_name"
  593. # )
  594. # continue
  595. # # Check if product exists
  596. # product = existing_products.get(item_id)
  597. # if not product:
  598. # attributes_failed += 1
  599. # errors.append(
  600. # f"Attribute_values Row {idx + 2}: Product with item_id '{item_id}' not found. "
  601. # "Make sure it exists in Products sheet."
  602. # )
  603. # continue
  604. # try:
  605. # attr_obj, created = ProductAttributeValue.objects.update_or_create(
  606. # product=product,
  607. # attribute_name=attribute_name,
  608. # defaults={'original_value': original_value}
  609. # )
  610. # if created:
  611. # attributes_created += 1
  612. # else:
  613. # attributes_updated += 1
  614. # except Exception as e:
  615. # attributes_failed += 1
  616. # errors.append(
  617. # f"Attribute_values Row {idx + 2} "
  618. # f"(item_id: {item_id}, attribute: {attribute_name}): {str(e)}"
  619. # )
  620. # # Prepare response
  621. # response_data = {
  622. # 'message': 'Upload completed successfully',
  623. # 'products': {
  624. # 'created': products_created,
  625. # 'updated': products_updated,
  626. # 'failed': products_failed,
  627. # 'total_processed': products_created + products_updated + products_failed
  628. # }
  629. # }
  630. # if has_attributes_sheet:
  631. # response_data['attribute_values'] = {
  632. # 'created': attributes_created,
  633. # 'updated': attributes_updated,
  634. # 'failed': attributes_failed,
  635. # 'total_processed': attributes_created + attributes_updated + attributes_failed
  636. # }
  637. # else:
  638. # response_data['attribute_values'] = {
  639. # 'message': 'Attribute_values sheet not found in Excel file'
  640. # }
  641. # if errors:
  642. # response_data['errors'] = errors[:50] # Limit to first 50 errors
  643. # if len(errors) > 50:
  644. # response_data['errors'].append(f"... and {len(errors) - 50} more errors")
  645. # # Determine status code
  646. # if products_failed > 0 or attributes_failed > 0:
  647. # status_code = status.HTTP_207_MULTI_STATUS
  648. # else:
  649. # status_code = status.HTTP_201_CREATED
  650. # return Response(response_data, status=status_code)
  651. # except pd.errors.EmptyDataError:
  652. # return Response({
  653. # 'error': 'The uploaded Excel file is empty or invalid'
  654. # }, status=status.HTTP_400_BAD_REQUEST)
  655. # except Exception as e:
  656. # return Response({
  657. # 'error': f'An error occurred while processing the file: {str(e)}'
  658. # }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  659. import logging
  660. import json
  661. from rest_framework.views import APIView
  662. from rest_framework.response import Response
  663. from rest_framework import status
  664. from rest_framework.parsers import MultiPartParser, FormParser
  665. from django.db import transaction
  666. from django.db.models import Prefetch, F
  667. import pandas as pd
  668. # Import ALL your models
  669. from .models import Product, ProductAttributeValue, ProductType, ProductAttribute, AttributePossibleValue
  670. from .services import ProductAttributeService
  671. from .ocr_service import OCRService
  672. from .visual_processing_service import VisualProcessingService
  673. from openpyxl import Workbook
  674. from openpyxl.styles import Font, PatternFill, Alignment
  675. from django.conf import settings
  676. import os
  677. import threading
  678. from datetime import datetime
  679. # --- Logging Setup ---
  680. # Define log and status file paths in MEDIA_ROOT
  681. LOG_FILE_PATH = os.path.join(settings.MEDIA_ROOT, 'excel_generation.log')
  682. STATUS_FILE_PATH = os.path.join(settings.MEDIA_ROOT, 'excel_generation_status.json')
  683. # Ensure the MEDIA_ROOT exists for files to be saved
  684. if not os.path.exists(settings.MEDIA_ROOT):
  685. os.makedirs(settings.MEDIA_ROOT)
  686. # Configure basic logging
  687. logging.basicConfig(
  688. filename=LOG_FILE_PATH,
  689. level=logging.INFO,
  690. format='%(asctime)s - %(levelname)s - %(message)s'
  691. )
  692. logger = logging.getLogger(__name__)
  693. # -------------------------------------------------------------------------------------------------
  694. def generate_product_excel_background():
  695. """
  696. Function to perform batch attribute extraction for all products and generate an Excel file.
  697. Runs in a background thread to avoid blocking the API response.
  698. Logs success/failure and saves a status file for external monitoring.
  699. """
  700. logger.info(f"[{datetime.now().isoformat()}] Starting background product Excel generation and attribute extraction.")
  701. successful = 0
  702. failed = 0
  703. results = [] # To store detailed extraction results for Excel sheet 2
  704. # Function to write status file (SUCCESS/FAILED)
  705. def write_status(status_type, error_msg=None):
  706. status_data = {
  707. "status": status_type,
  708. "timestamp": datetime.now().isoformat(),
  709. "products_processed": successful + failed,
  710. "products_successful": successful,
  711. "products_failed": failed,
  712. "excel_path": os.path.join(settings.MEDIA_URL, 'generated_products.xlsx') if status_type == "SUCCESS" else None,
  713. "log_path": os.path.join(settings.MEDIA_URL, 'excel_generation.log'),
  714. "error_message": error_msg
  715. }
  716. try:
  717. with open(STATUS_FILE_PATH, 'w') as f:
  718. json.dump(status_data, f, indent=4)
  719. except Exception as e:
  720. logger.exception(f"CRITICAL ERROR: Failed to write status file at {STATUS_FILE_PATH}: {e}")
  721. try:
  722. # 1. PREFETCH all necessary related data to minimize database queries
  723. # Prefetch possible values for mandatory attributes
  724. possible_values_prefetch = Prefetch(
  725. 'attributes',
  726. queryset=ProductAttribute.objects.filter(is_mandatory=True).prefetch_related('possible_values')
  727. )
  728. # Fetch all ProductTypes with their mandatory attributes and possible values
  729. all_product_types = ProductType.objects.prefetch_related(possible_values_prefetch)
  730. product_type_map = {
  731. pt.name: pt for pt in all_product_types
  732. }
  733. # Prepare product_list for batch extraction
  734. all_products = Product.objects.all()
  735. product_list = []
  736. for p in all_products:
  737. # mandatory_attrs will be the dictionary required by the service
  738. mandatory_attrs_dict = {}
  739. product_type_name = p.product_type.strip() if p.product_type else None
  740. if product_type_name and product_type_name in product_type_map:
  741. pt = product_type_map[product_type_name]
  742. # Build the mandatory_attrs dictionary: { "Attribute Name": ["Value 1", "Value 2"], ... }
  743. for attr in pt.attributes.all(): # .all() here works because we used Prefetch for 'attributes'
  744. # attr.possible_values.all() works because we used prefetch_related('possible_values')
  745. mandatory_attrs_dict[attr.name] = [
  746. pv.value for pv in attr.possible_values.all()
  747. ]
  748. product_list.append({
  749. "item_id": p.item_id,
  750. "product_type_name": product_type_name,
  751. "mandatory_attrs": mandatory_attrs_dict # <-- FIX: Pass the dictionary here
  752. })
  753. # Batch settings (using defaults)
  754. model = "llama-3.1-8b-instant"
  755. extract_additional = True
  756. process_image = False
  757. multiple = []
  758. threshold_abs = 0.65
  759. margin = 0.15
  760. use_dynamic_thresholds = True
  761. use_adaptive_margin = True
  762. use_semantic_clustering = True
  763. # Batch extraction logic
  764. item_ids = [p['item_id'] for p in product_list]
  765. products_queryset = Product.objects.filter(item_id__in=item_ids)
  766. product_map = {product.item_id: product for product in products_queryset}
  767. found_ids = set(product_map.keys())
  768. for product_entry in product_list:
  769. item_id = product_entry['item_id']
  770. # FIX: mandatory_attrs is now correctly a dictionary (or an empty dictionary)
  771. mandatory_attrs = product_entry['mandatory_attrs']
  772. if item_id not in found_ids:
  773. failed += 1
  774. results.append({
  775. "product_id": item_id,
  776. "error": "Product not found in database"
  777. })
  778. logger.warning(f"Product {item_id} not found in database. Skipping extraction.")
  779. continue
  780. product = product_map[item_id]
  781. try:
  782. title = product.product_name
  783. short_desc = product.product_short_description
  784. long_desc = product.product_long_description
  785. image_url = product.image_path
  786. ocr_results = None
  787. ocr_text = None
  788. visual_results = None
  789. if process_image and image_url:
  790. logger.info(f"Processing image for product {item_id}...")
  791. # OCR Processing
  792. ocr_service = OCRService()
  793. ocr_results = ocr_service.process_image(image_url)
  794. if ocr_results and ocr_results.get("detected_text"):
  795. # NOTE: Assuming ProductAttributeService.extract_attributes_from_ocr exists
  796. ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  797. ocr_results, model
  798. )
  799. ocr_results["extracted_attributes"] = ocr_attrs
  800. ocr_text = "\n".join([
  801. f"{item['text']} (confidence: {item['confidence']:.2f})"
  802. for item in ocr_results["detected_text"]
  803. ])
  804. # Visual Processing
  805. visual_service = VisualProcessingService()
  806. product_type_hint = product.product_type if product.product_type else None
  807. visual_results = visual_service.process_image(image_url, product_type_hint)
  808. if visual_results and visual_results.get('visual_attributes'):
  809. visual_results['visual_attributes'] = ProductAttributeService.format_visual_attributes(
  810. visual_results['visual_attributes']
  811. )
  812. logger.info(f"Image processing done for product {item_id}.")
  813. # Combine product text with source tracking
  814. product_text, source_map = ProductAttributeService.combine_product_text(
  815. title=title,
  816. short_desc=short_desc,
  817. long_desc=long_desc,
  818. ocr_text=ocr_text
  819. )
  820. # Attribute Extraction with source tracking
  821. extracted = ProductAttributeService.extract_attributes(
  822. product_text=product_text,
  823. mandatory_attrs=mandatory_attrs, # <-- This is now the dictionary with possible values
  824. source_map=source_map,
  825. model=model,
  826. extract_additional=extract_additional,
  827. multiple=multiple,
  828. threshold_abs=threshold_abs,
  829. margin=margin,
  830. use_dynamic_thresholds=use_dynamic_thresholds,
  831. use_adaptive_margin=use_adaptive_margin,
  832. use_semantic_clustering=use_semantic_clustering
  833. )
  834. result = {
  835. "product_id": item_id,
  836. "mandatory": extracted.get("mandatory", {}),
  837. "additional": extracted.get("additional", {}),
  838. }
  839. if ocr_results:
  840. result["ocr_results"] = ocr_results
  841. if visual_results:
  842. result["visual_results"] = visual_results
  843. results.append(result)
  844. successful += 1
  845. logger.info(f"Attribute extraction successful for product {item_id}.")
  846. except Exception as e:
  847. failed += 1
  848. results.append({
  849. "product_id": item_id,
  850. "error": str(e)
  851. })
  852. # Original Error: AttributeError: 'list' object has no attribute 'items'
  853. # This should now be fixed, but we keep the robust exception handling.
  854. logger.exception(f"Error during attribute extraction for product {item_id}.")
  855. logger.info(f"Batch extraction phase complete. Successful: {successful}, Failed: {failed}")
  856. # --------------------------------------------------------------------------------
  857. # Generate and save the Excel file (Unchanged)
  858. # --------------------------------------------------------------------------------
  859. wb = Workbook()
  860. # Sheet 1: Products (from DB)
  861. ws_products = wb.active
  862. ws_products.title = "Products"
  863. products_headers = ['ITEM ID', 'PRODUCT NAME', 'PRODUCT TYPE', 'Product Short Description', 'Product Long Description', 'image_path']
  864. header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
  865. header_font = Font(bold=True, color="FFFFFF")
  866. for col_num, header in enumerate(products_headers, 1):
  867. cell = ws_products.cell(row=1, column=col_num)
  868. cell.value = header
  869. cell.fill = header_fill
  870. cell.font = header_font
  871. cell.alignment = Alignment(horizontal="center", vertical="center")
  872. all_products_db = Product.objects.all()
  873. for row_num, product in enumerate(all_products_db, 2):
  874. ws_products.cell(row=row_num, column=1, value=product.item_id)
  875. ws_products.cell(row=row_num, column=2, value=product.product_name)
  876. ws_products.cell(row=row_num, column=3, value=product.product_type)
  877. ws_products.cell(row=row_num, column=4, value=product.product_short_description)
  878. ws_products.cell(row=row_num, column=5, value=product.product_long_description)
  879. ws_products.cell(row=row_num, column=6, value=product.image_path)
  880. # Adjust column widths
  881. for col_dim, width in zip(['A', 'B', 'C', 'D', 'E', 'F'], [15, 25, 15, 35, 50, 45]):
  882. ws_products.column_dimensions[col_dim].width = width
  883. # Sheet 2: Attribute_values (item_id, attribute_name, original_value, generated_value)
  884. ws_attributes = wb.create_sheet("Attribute_values")
  885. attributes_headers = ['item_id', 'attribute_name', 'original_value', 'generated_value']
  886. for col_num, header in enumerate(attributes_headers, 1):
  887. cell = ws_attributes.cell(row=1, column=col_num)
  888. cell.value = header
  889. cell.fill = header_fill
  890. cell.font = header_font
  891. cell.alignment = Alignment(horizontal="center", vertical="center")
  892. # Fetch all original attributes
  893. row_num = 2
  894. all_original_attrs = ProductAttributeValue.objects.all()
  895. # Create a lookup for original attributes by item_id and attribute_name
  896. original_attrs_lookup = {
  897. (attr.product.item_id, attr.attribute_name): attr.original_value
  898. for attr in all_original_attrs
  899. }
  900. # Add attributes (original and generated)
  901. processed_original_keys = set()
  902. for res in results:
  903. item_id = res["product_id"]
  904. if "error" in res:
  905. # Add existing original attributes for failed products to the sheet
  906. for (orig_item_id, orig_attr_name), orig_value in original_attrs_lookup.items():
  907. if orig_item_id == item_id:
  908. ws_attributes.cell(row=row_num, column=1, value=orig_item_id)
  909. ws_attributes.cell(row=row_num, column=2, value=orig_attr_name)
  910. ws_attributes.cell(row=row_num, column=3, value=orig_value)
  911. ws_attributes.cell(row=row_num, column=4, value=f"Extraction Failed: {res['error']}")
  912. processed_original_keys.add((orig_item_id, orig_attr_name))
  913. row_num += 1
  914. continue
  915. # Combine all generated attributes (mandatory, additional, OCR, visual)
  916. generated_attrs = {}
  917. for cat in ["mandatory", "additional"]:
  918. attrs = res.get(cat, {})
  919. for attr_name, values in attrs.items():
  920. for val in values:
  921. key = (item_id, attr_name)
  922. if key not in generated_attrs:
  923. generated_attrs[key] = []
  924. generated_attrs[key].append(f"{val['value']} (source: {val['source']})")
  925. # OCR extracted
  926. ocr = res.get("ocr_results")
  927. if ocr and "extracted_attributes" in ocr and isinstance(ocr["extracted_attributes"], dict):
  928. for attr_name, values in ocr["extracted_attributes"].items():
  929. for val in values:
  930. key = (item_id, attr_name)
  931. if key not in generated_attrs:
  932. generated_attrs[key] = []
  933. generated_attrs[key].append(f"{val['value']} (source: {val['source']})")
  934. # Visual extracted
  935. visual = res.get("visual_results")
  936. if visual and "visual_attributes" in visual:
  937. vis_attrs = visual["visual_attributes"]
  938. if isinstance(vis_attrs, dict):
  939. # Handle dict format where value might be a list of dicts or a single value
  940. for attr_name, values in vis_attrs.items():
  941. if not isinstance(values, list):
  942. values = [{"value": values, "source": "visual"}]
  943. for val in values:
  944. key = (item_id, attr_name)
  945. if key not in generated_attrs:
  946. generated_attrs[key] = []
  947. generated_attrs[key].append(f"{val['value']} (source: {val.get('source', 'visual')})")
  948. elif isinstance(vis_attrs, list):
  949. # Handle list of dicts format
  950. for item in vis_attrs:
  951. attr_name = item.get("attribute_name") or item.get("name")
  952. if not attr_name: continue
  953. value = item.get("value", "")
  954. source = item.get("source", "visual")
  955. key = (item_id, attr_name)
  956. if key not in generated_attrs:
  957. generated_attrs[key] = []
  958. generated_attrs[key].append(f"{value} (source: {source})")
  959. # Write attributes to Excel
  960. for (attr_item_id, attr_name), gen_values in generated_attrs.items():
  961. # Get original value from lookup (if it exists)
  962. original_value = original_attrs_lookup.get((attr_item_id, attr_name), "")
  963. # Combine multiple generated values into a single string
  964. generated_value = "; ".join(gen_values) if gen_values else ""
  965. # Write row
  966. ws_attributes.cell(row=row_num, column=1, value=attr_item_id)
  967. ws_attributes.cell(row=row_num, column=2, value=attr_name)
  968. ws_attributes.cell(row=row_num, column=3, value=original_value)
  969. ws_attributes.cell(row=row_num, column=4, value=generated_value)
  970. processed_original_keys.add((attr_item_id, attr_name))
  971. row_num += 1
  972. # Add original attributes that have no generated values for this item_id
  973. for (orig_item_id, orig_attr_name), orig_value in original_attrs_lookup.items():
  974. if orig_item_id == item_id and (orig_item_id, orig_attr_name) not in processed_original_keys:
  975. ws_attributes.cell(row=row_num, column=1, value=orig_item_id)
  976. ws_attributes.cell(row=row_num, column=2, value=orig_attr_name)
  977. ws_attributes.cell(row=row_num, column=3, value=orig_value)
  978. ws_attributes.cell(row=row_num, column=4, value="") # No generated value
  979. processed_original_keys.add((orig_item_id, orig_attr_name))
  980. row_num += 1
  981. # Add original attributes for products not included in the 'results' (e.g. if they didn't exist in product_list)
  982. # We assume all products are in product_list, so this step might be redundant, but safe for completeness.
  983. for (orig_item_id, orig_attr_name), orig_value in original_attrs_lookup.items():
  984. if (orig_item_id, orig_attr_name) not in processed_original_keys:
  985. ws_attributes.cell(row=row_num, column=1, value=orig_item_id)
  986. ws_attributes.cell(row=row_num, column=2, value=orig_attr_name)
  987. ws_attributes.cell(row=row_num, column=3, value=orig_value)
  988. ws_attributes.cell(row=row_num, column=4, value="Original value only (Product not processed in batch)")
  989. row_num += 1
  990. # Adjust column widths for attributes
  991. for col_dim, width in zip(['A', 'B', 'C', 'D'], [15, 35, 50, 50]):
  992. ws_attributes.column_dimensions[col_dim].width = width
  993. # Save the generated Excel (replace existing)
  994. save_path = os.path.join(settings.MEDIA_ROOT, 'generated_products.xlsx')
  995. wb.save(save_path)
  996. logger.info(f"Excel file successfully saved to {save_path}")
  997. # Write SUCCESS status
  998. write_status("SUCCESS")
  999. logger.info("Background task finished successfully.")
  1000. except Exception as e:
  1001. # Log the critical error and write FAILED status
  1002. logger.exception("CRITICAL ERROR during background Excel generation process.")
  1003. write_status("FAILED", error_msg=str(e))
  1004. # -------------------------------------------------------------------------------------------------
  1005. class ProductUploadExcelView(APIView):
  1006. """
  1007. POST API to upload an Excel file. (Unchanged)
  1008. """
  1009. parser_classes = (MultiPartParser, FormParser)
  1010. def post(self, request, *args, **kwargs):
  1011. file_obj = request.FILES.get('file')
  1012. if not file_obj:
  1013. return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
  1014. try:
  1015. # Read all sheets from Excel file
  1016. excel_file = pd.ExcelFile(file_obj)
  1017. # Check if required sheets exist
  1018. if 'Products' not in excel_file.sheet_names:
  1019. logger.error(f"Upload failed: Missing 'Products' sheet in file.")
  1020. return Response({
  1021. 'error': "Missing 'Products' sheet",
  1022. 'available_sheets': excel_file.sheet_names
  1023. }, status=status.HTTP_400_BAD_REQUEST)
  1024. # Read Products sheet
  1025. df_products = pd.read_excel(excel_file, sheet_name='Products')
  1026. df_products.columns = [c.strip().lower().replace(' ', '_') for c in df_products.columns]
  1027. # Check required columns for Products
  1028. expected_product_cols = {
  1029. 'item_id', 'product_name', 'product_long_description',
  1030. 'product_short_description', 'product_type', 'image_path'
  1031. }
  1032. if not expected_product_cols.issubset(df_products.columns):
  1033. logger.error(f"Upload failed: Missing required columns in Products sheet.")
  1034. return Response({
  1035. 'error': 'Missing required columns in Products sheet',
  1036. 'required_columns': list(expected_product_cols),
  1037. 'found_columns': list(df_products.columns)
  1038. }, status=status.HTTP_400_BAD_REQUEST)
  1039. # Read Attribute_values sheet if it exists
  1040. df_attributes = None
  1041. has_attributes_sheet = 'Attribute_values' in excel_file.sheet_names
  1042. if has_attributes_sheet:
  1043. df_attributes = pd.read_excel(excel_file, sheet_name='Attribute_values')
  1044. df_attributes.columns = [c.strip().lower().replace(' ', '_') for c in df_attributes.columns]
  1045. # Check required columns for Attribute_values
  1046. expected_attr_cols = {'item_id', 'attribute_name', 'original_value'}
  1047. if not expected_attr_cols.issubset(df_attributes.columns):
  1048. logger.error(f"Upload failed: Missing required columns in Attribute_values sheet.")
  1049. return Response({
  1050. 'error': 'Missing required columns in Attribute_values sheet',
  1051. 'required_columns': list(expected_attr_cols),
  1052. 'found_columns': list(df_attributes.columns)
  1053. }, status=status.HTTP_400_BAD_REQUEST)
  1054. # Initialize counters
  1055. products_created = 0
  1056. products_updated = 0
  1057. attributes_created = 0
  1058. attributes_updated = 0
  1059. products_failed = 0
  1060. attributes_failed = 0
  1061. errors = []
  1062. # Use transaction to ensure atomicity
  1063. with transaction.atomic():
  1064. # Process Products sheet
  1065. for idx, row in df_products.iterrows():
  1066. item_id = str(row.get('item_id', '')).strip()
  1067. product_type = str(row.get('product_type', '')).strip()
  1068. if not item_id:
  1069. products_failed += 1
  1070. errors.append(f"Products Row {idx + 2}: Missing item_id")
  1071. continue
  1072. try:
  1073. # Auto-create ProductType if provided and doesn't exist
  1074. if product_type:
  1075. ProductType.objects.get_or_create(name=product_type)
  1076. defaults = {
  1077. 'product_name': str(row.get('product_name', '')),
  1078. 'product_long_description': str(row.get('product_long_description', '')),
  1079. 'product_short_description': str(row.get('product_short_description', '')),
  1080. 'product_type': product_type,
  1081. 'image_path': str(row.get('image_path', '')),
  1082. }
  1083. obj, created = Product.objects.update_or_create(
  1084. item_id=item_id,
  1085. defaults=defaults
  1086. )
  1087. if created:
  1088. products_created += 1
  1089. else:
  1090. products_updated += 1
  1091. except Exception as e:
  1092. products_failed += 1
  1093. errors.append(f"Products Row {idx + 2} (item_id: {item_id}): {str(e)}")
  1094. logger.error(f"Error processing product {item_id} in Products sheet: {e}")
  1095. # Process Attribute_values sheet if it exists
  1096. if has_attributes_sheet and df_attributes is not None:
  1097. # Group by item_id to optimize lookups
  1098. item_ids_in_attrs = df_attributes['item_id'].astype(str).unique()
  1099. # Fetch all products at once
  1100. existing_products = {
  1101. p.item_id: p
  1102. for p in Product.objects.filter(item_id__in=item_ids_in_attrs)
  1103. }
  1104. for idx, row in df_attributes.iterrows():
  1105. item_id = str(row.get('item_id', '')).strip()
  1106. attribute_name = str(row.get('attribute_name', '')).strip()
  1107. original_value = str(row.get('original_value', '')).strip()
  1108. if not item_id or not attribute_name:
  1109. attributes_failed += 1
  1110. errors.append(
  1111. f"Attribute_values Row {idx + 2}: Missing item_id or attribute_name"
  1112. )
  1113. continue
  1114. # Check if product exists
  1115. product = existing_products.get(item_id)
  1116. if not product:
  1117. attributes_failed += 1
  1118. errors.append(
  1119. f"Attribute_values Row {idx + 2}: Product with item_id '{item_id}' not found. "
  1120. "Make sure it exists in Products sheet."
  1121. )
  1122. continue
  1123. try:
  1124. attr_obj, created = ProductAttributeValue.objects.update_or_create(
  1125. product=product,
  1126. attribute_name=attribute_name,
  1127. defaults={'original_value': original_value}
  1128. )
  1129. if created:
  1130. attributes_created += 1
  1131. else:
  1132. attributes_updated += 1
  1133. except Exception as e:
  1134. attributes_failed += 1
  1135. errors.append(
  1136. f"Attribute_values Row {idx + 2} "
  1137. f"(item_id: {item_id}, attribute: {attribute_name}): {str(e)}"
  1138. )
  1139. logger.error(f"Error processing attribute {attribute_name} for product {item_id}: {e}")
  1140. # Prepare response data
  1141. response_data = {
  1142. 'message': 'Upload completed',
  1143. 'products': {
  1144. 'created': products_created,
  1145. 'updated': products_updated,
  1146. 'failed': products_failed,
  1147. 'total_processed': products_created + products_updated + products_failed
  1148. },
  1149. 'attribute_values': {
  1150. 'created': attributes_created,
  1151. 'updated': attributes_updated,
  1152. 'failed': attributes_failed,
  1153. 'total_processed': attributes_created + attributes_updated + attributes_failed
  1154. } if has_attributes_sheet else {'message': 'Attribute_values sheet not found in Excel file'},
  1155. 'generated_excel_status': 'Excel generation started in the background.'
  1156. }
  1157. if errors:
  1158. response_data['errors'] = errors[:50]
  1159. if len(errors) > 50:
  1160. response_data['errors'].append(f"... and {len(errors) - 50} more errors")
  1161. # Determine status code for upload
  1162. upload_status = status.HTTP_201_CREATED if products_failed == 0 and attributes_failed == 0 else status.HTTP_207_MULTI_STATUS
  1163. # Start background thread for Excel generation if upload was successful
  1164. if products_failed == 0 and attributes_failed == 0:
  1165. logger.info("API call successful. Triggering background Excel generation thread.")
  1166. threading.Thread(target=generate_product_excel_background, daemon=True).start()
  1167. # Update response to provide monitoring paths
  1168. response_data['generated_excel_status'] = 'Background Excel generation triggered successfully.'
  1169. response_data['monitoring'] = {
  1170. 'excel_file': os.path.join(settings.MEDIA_URL, 'generated_products.xlsx'),
  1171. 'status_file': os.path.join(settings.MEDIA_URL, 'excel_generation_status.json'),
  1172. 'log_file': os.path.join(settings.MEDIA_URL, 'excel_generation.log'),
  1173. 'note': 'These files will be available once the background process completes.'
  1174. }
  1175. else:
  1176. logger.warning(f"API call finished with errors ({products_failed} products, {attributes_failed} attributes). Not triggering background excel generation.")
  1177. response_data['generated_excel_status'] = 'Background Excel generation was NOT triggered due to upload errors. Fix upload errors and re-upload.'
  1178. return Response(response_data, status=upload_status)
  1179. except pd.errors.EmptyDataError:
  1180. logger.error('The uploaded Excel file is empty or invalid.')
  1181. return Response({
  1182. 'error': 'The uploaded Excel file is empty or invalid'
  1183. }, status=status.HTTP_400_BAD_REQUEST)
  1184. except Exception as e:
  1185. logger.exception(f'An unexpected error occurred while processing the file.')
  1186. return Response({
  1187. 'error': f'An unexpected error occurred while processing the file: {str(e)}'
  1188. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1189. # Add this view to your views.py for downloading a template
  1190. from django.http import HttpResponse
  1191. from openpyxl import Workbook
  1192. from openpyxl.styles import Font, PatternFill, Alignment
  1193. from rest_framework.views import APIView
  1194. import io
  1195. class DownloadExcelTemplateView(APIView):
  1196. """
  1197. GET API to download an Excel template with two sheets:
  1198. 1. Products sheet with sample data
  1199. 2. Attribute_values sheet with sample data
  1200. """
  1201. def get(self, request):
  1202. # Create a new workbook
  1203. wb = Workbook()
  1204. # Remove default sheet
  1205. if 'Sheet' in wb.sheetnames:
  1206. wb.remove(wb['Sheet'])
  1207. # ===== Create Products Sheet =====
  1208. ws_products = wb.create_sheet("Products", 0)
  1209. # Define headers for Products
  1210. products_headers = [
  1211. 'ITEM ID',
  1212. 'PRODUCT NAME',
  1213. 'PRODUCT TYPE',
  1214. 'Product Short Description',
  1215. 'Product Long Description',
  1216. 'image_path'
  1217. ]
  1218. # Style for headers
  1219. header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
  1220. header_font = Font(bold=True, color="FFFFFF")
  1221. # Add headers to Products sheet
  1222. for col_num, header in enumerate(products_headers, 1):
  1223. cell = ws_products.cell(row=1, column=col_num)
  1224. cell.value = header
  1225. cell.fill = header_fill
  1226. cell.font = header_font
  1227. cell.alignment = Alignment(horizontal="center", vertical="center")
  1228. # Add sample data to Products sheet
  1229. sample_products = [
  1230. [
  1231. '3217373735',
  1232. 'Blue V-Neck T-Shirt',
  1233. 'Clothing',
  1234. 'Stylish blue t-shirt with v-neck design',
  1235. 'Premium quality cotton t-shirt featuring a classic v-neck design. Perfect for casual wear. Available in vibrant blue color.',
  1236. 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab'
  1237. ],
  1238. [
  1239. '1234567890',
  1240. 'Red Cotton Dress',
  1241. 'Clothing',
  1242. 'Beautiful red dress for special occasions',
  1243. 'Elegant red dress made from 100% cotton fabric. Features a flowing design perfect for summer events and parties.',
  1244. 'https://images.unsplash.com/photo-1595777457583-95e059d581b8'
  1245. ],
  1246. [
  1247. '9876543210',
  1248. 'Steel Screws Pack',
  1249. 'Hardware',
  1250. 'Pack of zinc plated steel screws',
  1251. 'Professional grade steel screws with zinc plating for corrosion resistance. Pack contains 50 pieces, 2 inch length, M6 thread size.',
  1252. 'https://images.unsplash.com/photo-1542272604-787c3835535d'
  1253. ]
  1254. ]
  1255. for row_num, row_data in enumerate(sample_products, 2):
  1256. for col_num, value in enumerate(row_data, 1):
  1257. ws_products.cell(row=row_num, column=col_num, value=value)
  1258. # Adjust column widths for Products sheet
  1259. ws_products.column_dimensions['A'].width = 15 # ITEM ID
  1260. ws_products.column_dimensions['B'].width = 25 # PRODUCT NAME
  1261. ws_products.column_dimensions['C'].width = 15 # PRODUCT TYPE
  1262. ws_products.column_dimensions['D'].width = 35 # Short Description
  1263. ws_products.column_dimensions['E'].width = 50 # Long Description
  1264. ws_products.column_dimensions['F'].width = 45 # image_path
  1265. # ===== Create Attribute_values Sheet =====
  1266. ws_attributes = wb.create_sheet("Attribute_values", 1)
  1267. # Define headers for Attribute_values
  1268. attributes_headers = ['item_id', 'attribute_name', 'original_value']
  1269. # Add headers to Attribute_values sheet
  1270. for col_num, header in enumerate(attributes_headers, 1):
  1271. cell = ws_attributes.cell(row=1, column=col_num)
  1272. cell.value = header
  1273. cell.fill = header_fill
  1274. cell.font = header_font
  1275. cell.alignment = Alignment(horizontal="center", vertical="center")
  1276. # Add sample data to Attribute_values sheet
  1277. sample_attributes = [
  1278. ['3217373735', 'Clothing Neck Style', 'V-Neck Square'],
  1279. ['3217373735', 'Condition', 'New with tags'],
  1280. ['3217373735', 'Material', '100% Cotton'],
  1281. ['3217373735', 'Color', 'Sky Blue'],
  1282. ['3217373735', 'Size', 'Medium'],
  1283. ['1234567890', 'Sleeve Length', 'Sleeveless'],
  1284. ['1234567890', 'Condition', 'Brand New'],
  1285. ['1234567890', 'Pattern', 'Solid'],
  1286. ['1234567890', 'Material', 'Cotton Blend'],
  1287. ['1234567890', 'Color', 'Crimson Red'],
  1288. ['9876543210', 'Material', 'Stainless Steel'],
  1289. ['9876543210', 'Thread Size', 'M6'],
  1290. ['9876543210', 'Length', '2 inches'],
  1291. ['9876543210', 'Coating', 'Zinc Plated'],
  1292. ['9876543210', 'Package Quantity', '50 pieces'],
  1293. ]
  1294. for row_num, row_data in enumerate(sample_attributes, 2):
  1295. for col_num, value in enumerate(row_data, 1):
  1296. ws_attributes.cell(row=row_num, column=col_num, value=value)
  1297. # Adjust column widths for Attribute_values sheet
  1298. ws_attributes.column_dimensions['A'].width = 15 # item_id
  1299. ws_attributes.column_dimensions['B'].width = 25 # attribute_name
  1300. ws_attributes.column_dimensions['C'].width = 30 # original_value
  1301. # Add instructions sheet
  1302. ws_instructions = wb.create_sheet("Instructions", 2)
  1303. instructions_text = [
  1304. ['Excel Upload Instructions', ''],
  1305. ['', ''],
  1306. ['Sheet 1: Products', ''],
  1307. ['- Contains product basic information', ''],
  1308. ['- All columns are required', ''],
  1309. ['- ITEM ID must be unique', ''],
  1310. ['', ''],
  1311. ['Sheet 2: Attribute_values', ''],
  1312. ['- Contains original/manual attribute values', ''],
  1313. ['- item_id must match an ITEM ID from Products sheet', ''],
  1314. ['- Multiple rows can have the same item_id (for different attributes)', ''],
  1315. ['- Each attribute per product should be on a separate row', ''],
  1316. ['', ''],
  1317. ['Upload Process:', ''],
  1318. ['1. Fill in your product data in the Products sheet', ''],
  1319. ['2. Fill in attribute values in the Attribute_values sheet', ''],
  1320. ['3. Ensure item_id values match between both sheets', ''],
  1321. ['4. Save the file and upload via API', ''],
  1322. ['', ''],
  1323. ['Notes:', ''],
  1324. ['- Do not change sheet names (must be "Products" and "Attribute_values")', ''],
  1325. ['- Do not change column header names', ''],
  1326. ['- You can delete the sample data rows', ''],
  1327. ['- You can delete this Instructions sheet before uploading', ''],
  1328. ]
  1329. for row_num, row_data in enumerate(instructions_text, 1):
  1330. ws_instructions.cell(row=row_num, column=1, value=row_data[0])
  1331. if row_num == 1:
  1332. cell = ws_instructions.cell(row=row_num, column=1)
  1333. cell.font = Font(bold=True, size=14)
  1334. ws_instructions.column_dimensions['A'].width = 60
  1335. # Save to BytesIO
  1336. output = io.BytesIO()
  1337. wb.save(output)
  1338. output.seek(0)
  1339. # Create response
  1340. response = HttpResponse(
  1341. output.getvalue(),
  1342. content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  1343. )
  1344. response['Content-Disposition'] = 'attachment; filename=product_upload_template.xlsx'
  1345. return response
  1346. class DownloadProductsWithAttributesExcelView(APIView):
  1347. """
  1348. GET API to download existing products with their attribute values as Excel.
  1349. Useful for users to update existing data.
  1350. """
  1351. def get(self, request):
  1352. from .models import Product, ProductAttributeValue
  1353. # Create workbook
  1354. wb = Workbook()
  1355. if 'Sheet' in wb.sheetnames:
  1356. wb.remove(wb['Sheet'])
  1357. # ===== Products Sheet =====
  1358. ws_products = wb.create_sheet("Products", 0)
  1359. # Headers
  1360. products_headers = [
  1361. 'ITEM ID', 'PRODUCT NAME', 'PRODUCT TYPE',
  1362. 'Product Short Description', 'Product Long Description', 'image_path'
  1363. ]
  1364. header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
  1365. header_font = Font(bold=True, color="FFFFFF")
  1366. for col_num, header in enumerate(products_headers, 1):
  1367. cell = ws_products.cell(row=1, column=col_num)
  1368. cell.value = header
  1369. cell.fill = header_fill
  1370. cell.font = header_font
  1371. cell.alignment = Alignment(horizontal="center", vertical="center")
  1372. # Fetch and add product data
  1373. products = Product.objects.all()
  1374. for row_num, product in enumerate(products, 2):
  1375. ws_products.cell(row=row_num, column=1, value=product.item_id)
  1376. ws_products.cell(row=row_num, column=2, value=product.product_name)
  1377. ws_products.cell(row=row_num, column=3, value=product.product_type)
  1378. ws_products.cell(row=row_num, column=4, value=product.product_short_description)
  1379. ws_products.cell(row=row_num, column=5, value=product.product_long_description)
  1380. ws_products.cell(row=row_num, column=6, value=product.image_path)
  1381. # Adjust widths
  1382. ws_products.column_dimensions['A'].width = 15
  1383. ws_products.column_dimensions['B'].width = 25
  1384. ws_products.column_dimensions['C'].width = 15
  1385. ws_products.column_dimensions['D'].width = 35
  1386. ws_products.column_dimensions['E'].width = 50
  1387. ws_products.column_dimensions['F'].width = 45
  1388. # ===== Attribute_values Sheet =====
  1389. ws_attributes = wb.create_sheet("Attribute_values", 1)
  1390. attributes_headers = ['item_id', 'attribute_name', 'original_value']
  1391. for col_num, header in enumerate(attributes_headers, 1):
  1392. cell = ws_attributes.cell(row=1, column=col_num)
  1393. cell.value = header
  1394. cell.fill = header_fill
  1395. cell.font = header_font
  1396. cell.alignment = Alignment(horizontal="center", vertical="center")
  1397. # Fetch and add attribute values
  1398. attributes = ProductAttributeValue.objects.select_related('product').all()
  1399. for row_num, attr in enumerate(attributes, 2):
  1400. ws_attributes.cell(row=row_num, column=1, value=attr.product.item_id)
  1401. ws_attributes.cell(row=row_num, column=2, value=attr.attribute_name)
  1402. ws_attributes.cell(row=row_num, column=3, value=attr.original_value)
  1403. ws_attributes.column_dimensions['A'].width = 15
  1404. ws_attributes.column_dimensions['B'].width = 25
  1405. ws_attributes.column_dimensions['C'].width = 30
  1406. # Save to BytesIO
  1407. output = io.BytesIO()
  1408. wb.save(output)
  1409. output.seek(0)
  1410. response = HttpResponse(
  1411. output.getvalue(),
  1412. content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  1413. )
  1414. response['Content-Disposition'] = 'attachment; filename=products_export.xlsx'
  1415. return response
  1416. class ProductAttributesUploadView(APIView):
  1417. """
  1418. POST API to upload an Excel file and add mandatory/additional attributes
  1419. for product types with possible values.
  1420. """
  1421. parser_classes = (MultiPartParser, FormParser)
  1422. def post(self, request):
  1423. file_obj = request.FILES.get('file')
  1424. if not file_obj:
  1425. return Response({"error": "No file provided."}, status=status.HTTP_400_BAD_REQUEST)
  1426. try:
  1427. df = pd.read_excel(file_obj)
  1428. required_columns = {'product_type', 'attribute_name', 'is_mandatory', 'possible_values'}
  1429. if not required_columns.issubset(df.columns):
  1430. return Response({
  1431. "error": f"Missing required columns. Found: {list(df.columns)}"
  1432. }, status=status.HTTP_400_BAD_REQUEST)
  1433. for _, row in df.iterrows():
  1434. product_type_name = str(row['product_type']).strip()
  1435. attr_name = str(row['attribute_name']).strip()
  1436. is_mandatory = str(row['is_mandatory']).strip().lower() in ['yes', 'true', '1']
  1437. possible_values = str(row.get('possible_values', '')).strip()
  1438. # Get or create product type
  1439. product_type, _ = ProductType.objects.get_or_create(name=product_type_name)
  1440. # Get or create attribute
  1441. attribute, _ = ProductAttribute.objects.get_or_create(
  1442. product_type=product_type,
  1443. name=attr_name,
  1444. defaults={'is_mandatory': is_mandatory}
  1445. )
  1446. attribute.is_mandatory = is_mandatory
  1447. attribute.save()
  1448. # Handle possible values
  1449. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  1450. if possible_values:
  1451. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  1452. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  1453. return Response({"message": "Attributes uploaded successfully."}, status=status.HTTP_201_CREATED)
  1454. except Exception as e:
  1455. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1456. class ProductTypeAttributesView(APIView):
  1457. """
  1458. API to view, create, update, and delete product type attributes and their possible values.
  1459. Also supports dynamic product type creation.
  1460. """
  1461. def get(self, request):
  1462. """
  1463. Retrieve all product types with their attributes and possible values.
  1464. """
  1465. product_types = ProductType.objects.all()
  1466. serializer = ProductTypeSerializer(product_types, many=True)
  1467. # Transform the serialized data into the requested format
  1468. result = []
  1469. for pt in serializer.data:
  1470. for attr in pt['attributes']:
  1471. result.append({
  1472. 'product_type': pt['name'],
  1473. 'attribute_name': attr['name'],
  1474. 'is_mandatory': 'Yes' if attr['is_mandatory'] else 'No',
  1475. 'possible_values': ', '.join([pv['value'] for pv in attr['possible_values']])
  1476. })
  1477. return Response(result, status=status.HTTP_200_OK)
  1478. def post(self, request):
  1479. """
  1480. Create a new product type or attribute with possible values.
  1481. Expected payload example:
  1482. {
  1483. "product_type": "Hardware Screws",
  1484. "attribute_name": "Material",
  1485. "is_mandatory": "Yes",
  1486. "possible_values": "Steel, Zinc Plated, Stainless Steel"
  1487. }
  1488. """
  1489. try:
  1490. product_type_name = request.data.get('product_type')
  1491. attribute_name = request.data.get('attribute_name', '')
  1492. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  1493. possible_values = request.data.get('possible_values', '')
  1494. if not product_type_name:
  1495. return Response({
  1496. "error": "product_type is required"
  1497. }, status=status.HTTP_400_BAD_REQUEST)
  1498. with transaction.atomic():
  1499. # Get or create product type
  1500. product_type, created = ProductType.objects.get_or_create(name=product_type_name)
  1501. if created and not attribute_name:
  1502. return Response({
  1503. "message": f"Product type '{product_type_name}' created successfully",
  1504. "data": {"product_type": product_type_name}
  1505. }, status=status.HTTP_201_CREATED)
  1506. if attribute_name:
  1507. # Create attribute
  1508. attribute, attr_created = ProductAttribute.objects.get_or_create(
  1509. product_type=product_type,
  1510. name=attribute_name,
  1511. defaults={'is_mandatory': is_mandatory}
  1512. )
  1513. if not attr_created:
  1514. return Response({
  1515. "error": f"Attribute '{attribute_name}' already exists for product type '{product_type_name}'"
  1516. }, status=status.HTTP_400_BAD_REQUEST)
  1517. # Handle possible values
  1518. if possible_values:
  1519. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  1520. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  1521. return Response({
  1522. "message": "Attribute created successfully",
  1523. "data": {
  1524. "product_type": product_type_name,
  1525. "attribute_name": attribute_name,
  1526. "is_mandatory": "Yes" if is_mandatory else "No",
  1527. "possible_values": possible_values
  1528. }
  1529. }, status=status.HTTP_201_CREATED)
  1530. return Response({
  1531. "message": f"Product type '{product_type_name}' already exists",
  1532. "data": {"product_type": product_type_name}
  1533. }, status=status.HTTP_200_OK)
  1534. except Exception as e:
  1535. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1536. def put(self, request):
  1537. """
  1538. Update an existing product type attribute and its possible values.
  1539. Expected payload example:
  1540. {
  1541. "product_type": "Hardware Screws",
  1542. "attribute_name": "Material",
  1543. "is_mandatory": "Yes",
  1544. "possible_values": "Steel, Zinc Plated, Stainless Steel, Brass"
  1545. }
  1546. """
  1547. try:
  1548. product_type_name = request.data.get('product_type')
  1549. attribute_name = request.data.get('attribute_name')
  1550. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  1551. possible_values = request.data.get('possible_values', '')
  1552. if not all([product_type_name, attribute_name]):
  1553. return Response({
  1554. "error": "product_type and attribute_name are required"
  1555. }, status=status.HTTP_400_BAD_REQUEST)
  1556. with transaction.atomic():
  1557. try:
  1558. product_type = ProductType.objects.get(name=product_type_name)
  1559. attribute = ProductAttribute.objects.get(
  1560. product_type=product_type,
  1561. name=attribute_name
  1562. )
  1563. except ProductType.DoesNotExist:
  1564. return Response({
  1565. "error": f"Product type '{product_type_name}' not found"
  1566. }, status=status.HTTP_404_NOT_FOUND)
  1567. except ProductAttribute.DoesNotExist:
  1568. return Response({
  1569. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  1570. }, status=status.HTTP_404_NOT_FOUND)
  1571. # Update attribute
  1572. attribute.is_mandatory = is_mandatory
  1573. attribute.save()
  1574. # Update possible values
  1575. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  1576. if possible_values:
  1577. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  1578. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  1579. return Response({
  1580. "message": "Attribute updated successfully",
  1581. "data": {
  1582. "product_type": product_type_name,
  1583. "attribute_name": attribute_name,
  1584. "is_mandatory": "Yes" if is_mandatory else "No",
  1585. "possible_values": possible_values
  1586. }
  1587. }, status=status.HTTP_200_OK)
  1588. except Exception as e:
  1589. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1590. def delete(self, request):
  1591. """
  1592. Delete a product type or a specific attribute.
  1593. Expected payload example:
  1594. {
  1595. "product_type": "Hardware Screws",
  1596. "attribute_name": "Material"
  1597. }
  1598. """
  1599. try:
  1600. product_type_name = request.data.get('product_type')
  1601. attribute_name = request.data.get('attribute_name', '')
  1602. if not product_type_name:
  1603. return Response({
  1604. "error": "product_type is required"
  1605. }, status=status.HTTP_400_BAD_REQUEST)
  1606. with transaction.atomic():
  1607. try:
  1608. product_type = ProductType.objects.get(name=product_type_name)
  1609. except ProductType.DoesNotExist:
  1610. return Response({
  1611. "error": f"Product type '{product_type_name}' not found"
  1612. }, status=status.HTTP_404_NOT_FOUND)
  1613. if attribute_name:
  1614. # Delete specific attribute
  1615. try:
  1616. attribute = ProductAttribute.objects.get(
  1617. product_type=product_type,
  1618. name=attribute_name
  1619. )
  1620. attribute.delete()
  1621. return Response({
  1622. "message": f"Attribute '{attribute_name}' deleted successfully from product type '{product_type_name}'"
  1623. }, status=status.HTTP_200_OK)
  1624. except ProductAttribute.DoesNotExist:
  1625. return Response({
  1626. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  1627. }, status=status.HTTP_404_NOT_FOUND)
  1628. else:
  1629. # Delete entire product type
  1630. product_type.delete()
  1631. return Response({
  1632. "message": f"Product type '{product_type_name}' and all its attributes deleted successfully"
  1633. }, status=status.HTTP_200_OK)
  1634. except Exception as e:
  1635. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1636. class ProductTypeListView(APIView):
  1637. """
  1638. GET API to list all product types (only names).
  1639. """
  1640. def get(self, request):
  1641. product_types = ProductType.objects.values_list('name', flat=True)
  1642. return Response({"product_types": list(product_types)}, status=status.HTTP_200_OK)
  1643. # Add these views to your views.py
  1644. from rest_framework.views import APIView
  1645. from rest_framework.response import Response
  1646. from rest_framework import status
  1647. from rest_framework.parsers import MultiPartParser, FormParser
  1648. from django.db import transaction
  1649. import pandas as pd
  1650. from .models import Product, ProductAttributeValue
  1651. from .serializers import (
  1652. ProductAttributeValueSerializer,
  1653. ProductAttributeValueInputSerializer,
  1654. BulkProductAttributeValueSerializer,
  1655. ProductWithAttributesSerializer
  1656. )
  1657. class ProductAttributeValueView(APIView):
  1658. """
  1659. API to manage manually entered original attribute values.
  1660. GET: Retrieve all attribute values for a product
  1661. POST: Create or update attribute values for a product
  1662. DELETE: Delete attribute values
  1663. """
  1664. def get(self, request):
  1665. """
  1666. Get original attribute values for a specific product or all products.
  1667. Query params: item_id (optional)
  1668. """
  1669. item_id = request.query_params.get('item_id')
  1670. if item_id:
  1671. try:
  1672. product = Product.objects.get(item_id=item_id)
  1673. values = ProductAttributeValue.objects.filter(product=product)
  1674. serializer = ProductAttributeValueSerializer(values, many=True)
  1675. return Response({
  1676. "item_id": item_id,
  1677. "attributes": serializer.data
  1678. }, status=status.HTTP_200_OK)
  1679. except Product.DoesNotExist:
  1680. return Response({
  1681. "error": f"Product with item_id '{item_id}' not found"
  1682. }, status=status.HTTP_404_NOT_FOUND)
  1683. else:
  1684. # Return all attribute values grouped by product
  1685. values = ProductAttributeValue.objects.all().select_related('product')
  1686. serializer = ProductAttributeValueSerializer(values, many=True)
  1687. return Response(serializer.data, status=status.HTTP_200_OK)
  1688. def post(self, request):
  1689. """
  1690. Create or update original attribute value for a product.
  1691. Expected payload:
  1692. {
  1693. "item_id": "3217373735",
  1694. "attribute_name": "Clothing Neck Style",
  1695. "original_value": "V-Neck Square"
  1696. }
  1697. """
  1698. serializer = ProductAttributeValueInputSerializer(data=request.data)
  1699. if not serializer.is_valid():
  1700. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  1701. validated_data = serializer.validated_data
  1702. item_id = validated_data['item_id']
  1703. attribute_name = validated_data['attribute_name']
  1704. original_value = validated_data['original_value']
  1705. try:
  1706. product = Product.objects.get(item_id=item_id)
  1707. except Product.DoesNotExist:
  1708. return Response({
  1709. "error": f"Product with item_id '{item_id}' not found"
  1710. }, status=status.HTTP_404_NOT_FOUND)
  1711. # Create or update the attribute value
  1712. attr_value, created = ProductAttributeValue.objects.update_or_create(
  1713. product=product,
  1714. attribute_name=attribute_name,
  1715. defaults={'original_value': original_value}
  1716. )
  1717. response_serializer = ProductAttributeValueSerializer(attr_value)
  1718. return Response({
  1719. "message": "Attribute value created" if created else "Attribute value updated",
  1720. "data": response_serializer.data
  1721. }, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
  1722. def delete(self, request):
  1723. """
  1724. Delete original attribute value(s).
  1725. Expected payload:
  1726. {
  1727. "item_id": "3217373735",
  1728. "attribute_name": "Clothing Neck Style" # Optional, if not provided deletes all for product
  1729. }
  1730. """
  1731. item_id = request.data.get('item_id')
  1732. attribute_name = request.data.get('attribute_name')
  1733. if not item_id:
  1734. return Response({
  1735. "error": "item_id is required"
  1736. }, status=status.HTTP_400_BAD_REQUEST)
  1737. try:
  1738. product = Product.objects.get(item_id=item_id)
  1739. except Product.DoesNotExist:
  1740. return Response({
  1741. "error": f"Product with item_id '{item_id}' not found"
  1742. }, status=status.HTTP_404_NOT_FOUND)
  1743. if attribute_name:
  1744. # Delete specific attribute
  1745. deleted_count, _ = ProductAttributeValue.objects.filter(
  1746. product=product,
  1747. attribute_name=attribute_name
  1748. ).delete()
  1749. if deleted_count == 0:
  1750. return Response({
  1751. "error": f"Attribute '{attribute_name}' not found for product '{item_id}'"
  1752. }, status=status.HTTP_404_NOT_FOUND)
  1753. return Response({
  1754. "message": f"Attribute '{attribute_name}' deleted successfully"
  1755. }, status=status.HTTP_200_OK)
  1756. else:
  1757. # Delete all attributes for product
  1758. deleted_count, _ = ProductAttributeValue.objects.filter(product=product).delete()
  1759. return Response({
  1760. "message": f"Deleted {deleted_count} attribute(s) for product '{item_id}'"
  1761. }, status=status.HTTP_200_OK)
  1762. class BulkProductAttributeValueView(APIView):
  1763. """
  1764. API for bulk operations on original attribute values.
  1765. POST: Create/update multiple attribute values at once
  1766. """
  1767. def post(self, request):
  1768. """
  1769. Bulk create or update attribute values for multiple products.
  1770. Expected payload:
  1771. {
  1772. "products": [
  1773. {
  1774. "item_id": "3217373735",
  1775. "attributes": {
  1776. "Clothing Neck Style": "V-Neck Square",
  1777. "Condition": "New with tags"
  1778. }
  1779. },
  1780. {
  1781. "item_id": "1234567890",
  1782. "attributes": {
  1783. "Material": "Cotton",
  1784. "Size": "L"
  1785. }
  1786. }
  1787. ]
  1788. }
  1789. """
  1790. products_data = request.data.get('products', [])
  1791. if not products_data:
  1792. return Response({
  1793. "error": "products list is required"
  1794. }, status=status.HTTP_400_BAD_REQUEST)
  1795. results = []
  1796. successful = 0
  1797. failed = 0
  1798. with transaction.atomic():
  1799. for product_data in products_data:
  1800. serializer = BulkProductAttributeValueSerializer(data=product_data)
  1801. if not serializer.is_valid():
  1802. failed += 1
  1803. results.append({
  1804. "item_id": product_data.get('item_id'),
  1805. "status": "failed",
  1806. "error": serializer.errors
  1807. })
  1808. continue
  1809. validated_data = serializer.validated_data
  1810. item_id = validated_data['item_id']
  1811. attributes = validated_data['attributes']
  1812. try:
  1813. product = Product.objects.get(item_id=item_id)
  1814. created_count = 0
  1815. updated_count = 0
  1816. for attr_name, original_value in attributes.items():
  1817. _, created = ProductAttributeValue.objects.update_or_create(
  1818. product=product,
  1819. attribute_name=attr_name,
  1820. defaults={'original_value': original_value}
  1821. )
  1822. if created:
  1823. created_count += 1
  1824. else:
  1825. updated_count += 1
  1826. successful += 1
  1827. results.append({
  1828. "item_id": item_id,
  1829. "status": "success",
  1830. "created": created_count,
  1831. "updated": updated_count
  1832. })
  1833. except Product.DoesNotExist:
  1834. failed += 1
  1835. results.append({
  1836. "item_id": item_id,
  1837. "status": "failed",
  1838. "error": f"Product not found"
  1839. })
  1840. return Response({
  1841. "results": results,
  1842. "total_products": len(products_data),
  1843. "successful": successful,
  1844. "failed": failed
  1845. }, status=status.HTTP_200_OK)
  1846. class ProductListWithAttributesView(APIView):
  1847. """
  1848. GET API to list all products with their original attribute values.
  1849. """
  1850. def get(self, request):
  1851. item_id = request.query_params.get('item_id')
  1852. if item_id:
  1853. try:
  1854. product = Product.objects.get(item_id=item_id)
  1855. serializer = ProductWithAttributesSerializer(product)
  1856. return Response(serializer.data, status=status.HTTP_200_OK)
  1857. except Product.DoesNotExist:
  1858. return Response({
  1859. "error": f"Product with item_id '{item_id}' not found"
  1860. }, status=status.HTTP_404_NOT_FOUND)
  1861. else:
  1862. products = Product.objects.all()
  1863. serializer = ProductWithAttributesSerializer(products, many=True)
  1864. return Response(serializer.data, status=status.HTTP_200_OK)