views.py 65 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592
  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. # Add this view to your views.py for downloading a template
  660. from django.http import HttpResponse
  661. from openpyxl import Workbook
  662. from openpyxl.styles import Font, PatternFill, Alignment
  663. from rest_framework.views import APIView
  664. import io
  665. class DownloadExcelTemplateView(APIView):
  666. """
  667. GET API to download an Excel template with two sheets:
  668. 1. Products sheet with sample data
  669. 2. Attribute_values sheet with sample data
  670. """
  671. def get(self, request):
  672. # Create a new workbook
  673. wb = Workbook()
  674. # Remove default sheet
  675. if 'Sheet' in wb.sheetnames:
  676. wb.remove(wb['Sheet'])
  677. # ===== Create Products Sheet =====
  678. ws_products = wb.create_sheet("Products", 0)
  679. # Define headers for Products
  680. products_headers = [
  681. 'ITEM ID',
  682. 'PRODUCT NAME',
  683. 'PRODUCT TYPE',
  684. 'Product Short Description',
  685. 'Product Long Description',
  686. 'image_path'
  687. ]
  688. # Style for headers
  689. header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
  690. header_font = Font(bold=True, color="FFFFFF")
  691. # Add headers to Products sheet
  692. for col_num, header in enumerate(products_headers, 1):
  693. cell = ws_products.cell(row=1, column=col_num)
  694. cell.value = header
  695. cell.fill = header_fill
  696. cell.font = header_font
  697. cell.alignment = Alignment(horizontal="center", vertical="center")
  698. # Add sample data to Products sheet
  699. sample_products = [
  700. [
  701. '3217373735',
  702. 'Blue V-Neck T-Shirt',
  703. 'Clothing',
  704. 'Stylish blue t-shirt with v-neck design',
  705. 'Premium quality cotton t-shirt featuring a classic v-neck design. Perfect for casual wear. Available in vibrant blue color.',
  706. 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab'
  707. ],
  708. [
  709. '1234567890',
  710. 'Red Cotton Dress',
  711. 'Clothing',
  712. 'Beautiful red dress for special occasions',
  713. 'Elegant red dress made from 100% cotton fabric. Features a flowing design perfect for summer events and parties.',
  714. 'https://images.unsplash.com/photo-1595777457583-95e059d581b8'
  715. ],
  716. [
  717. '9876543210',
  718. 'Steel Screws Pack',
  719. 'Hardware',
  720. 'Pack of zinc plated steel screws',
  721. 'Professional grade steel screws with zinc plating for corrosion resistance. Pack contains 50 pieces, 2 inch length, M6 thread size.',
  722. 'https://images.unsplash.com/photo-1542272604-787c3835535d'
  723. ]
  724. ]
  725. for row_num, row_data in enumerate(sample_products, 2):
  726. for col_num, value in enumerate(row_data, 1):
  727. ws_products.cell(row=row_num, column=col_num, value=value)
  728. # Adjust column widths for Products sheet
  729. ws_products.column_dimensions['A'].width = 15 # ITEM ID
  730. ws_products.column_dimensions['B'].width = 25 # PRODUCT NAME
  731. ws_products.column_dimensions['C'].width = 15 # PRODUCT TYPE
  732. ws_products.column_dimensions['D'].width = 35 # Short Description
  733. ws_products.column_dimensions['E'].width = 50 # Long Description
  734. ws_products.column_dimensions['F'].width = 45 # image_path
  735. # ===== Create Attribute_values Sheet =====
  736. ws_attributes = wb.create_sheet("Attribute_values", 1)
  737. # Define headers for Attribute_values
  738. attributes_headers = ['item_id', 'attribute_name', 'original_value']
  739. # Add headers to Attribute_values sheet
  740. for col_num, header in enumerate(attributes_headers, 1):
  741. cell = ws_attributes.cell(row=1, column=col_num)
  742. cell.value = header
  743. cell.fill = header_fill
  744. cell.font = header_font
  745. cell.alignment = Alignment(horizontal="center", vertical="center")
  746. # Add sample data to Attribute_values sheet
  747. sample_attributes = [
  748. ['3217373735', 'Clothing Neck Style', 'V-Neck Square'],
  749. ['3217373735', 'Condition', 'New with tags'],
  750. ['3217373735', 'Material', '100% Cotton'],
  751. ['3217373735', 'Color', 'Sky Blue'],
  752. ['3217373735', 'Size', 'Medium'],
  753. ['1234567890', 'Sleeve Length', 'Sleeveless'],
  754. ['1234567890', 'Condition', 'Brand New'],
  755. ['1234567890', 'Pattern', 'Solid'],
  756. ['1234567890', 'Material', 'Cotton Blend'],
  757. ['1234567890', 'Color', 'Crimson Red'],
  758. ['9876543210', 'Material', 'Stainless Steel'],
  759. ['9876543210', 'Thread Size', 'M6'],
  760. ['9876543210', 'Length', '2 inches'],
  761. ['9876543210', 'Coating', 'Zinc Plated'],
  762. ['9876543210', 'Package Quantity', '50 pieces'],
  763. ]
  764. for row_num, row_data in enumerate(sample_attributes, 2):
  765. for col_num, value in enumerate(row_data, 1):
  766. ws_attributes.cell(row=row_num, column=col_num, value=value)
  767. # Adjust column widths for Attribute_values sheet
  768. ws_attributes.column_dimensions['A'].width = 15 # item_id
  769. ws_attributes.column_dimensions['B'].width = 25 # attribute_name
  770. ws_attributes.column_dimensions['C'].width = 30 # original_value
  771. # Add instructions sheet
  772. ws_instructions = wb.create_sheet("Instructions", 2)
  773. instructions_text = [
  774. ['Excel Upload Instructions', ''],
  775. ['', ''],
  776. ['Sheet 1: Products', ''],
  777. ['- Contains product basic information', ''],
  778. ['- All columns are required', ''],
  779. ['- ITEM ID must be unique', ''],
  780. ['', ''],
  781. ['Sheet 2: Attribute_values', ''],
  782. ['- Contains original/manual attribute values', ''],
  783. ['- item_id must match an ITEM ID from Products sheet', ''],
  784. ['- Multiple rows can have the same item_id (for different attributes)', ''],
  785. ['- Each attribute per product should be on a separate row', ''],
  786. ['', ''],
  787. ['Upload Process:', ''],
  788. ['1. Fill in your product data in the Products sheet', ''],
  789. ['2. Fill in attribute values in the Attribute_values sheet', ''],
  790. ['3. Ensure item_id values match between both sheets', ''],
  791. ['4. Save the file and upload via API', ''],
  792. ['', ''],
  793. ['Notes:', ''],
  794. ['- Do not change sheet names (must be "Products" and "Attribute_values")', ''],
  795. ['- Do not change column header names', ''],
  796. ['- You can delete the sample data rows', ''],
  797. ['- You can delete this Instructions sheet before uploading', ''],
  798. ]
  799. for row_num, row_data in enumerate(instructions_text, 1):
  800. ws_instructions.cell(row=row_num, column=1, value=row_data[0])
  801. if row_num == 1:
  802. cell = ws_instructions.cell(row=row_num, column=1)
  803. cell.font = Font(bold=True, size=14)
  804. ws_instructions.column_dimensions['A'].width = 60
  805. # Save to BytesIO
  806. output = io.BytesIO()
  807. wb.save(output)
  808. output.seek(0)
  809. # Create response
  810. response = HttpResponse(
  811. output.getvalue(),
  812. content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  813. )
  814. response['Content-Disposition'] = 'attachment; filename=product_upload_template.xlsx'
  815. return response
  816. class DownloadProductsWithAttributesExcelView(APIView):
  817. """
  818. GET API to download existing products with their attribute values as Excel.
  819. Useful for users to update existing data.
  820. """
  821. def get(self, request):
  822. from .models import Product, ProductAttributeValue
  823. # Create workbook
  824. wb = Workbook()
  825. if 'Sheet' in wb.sheetnames:
  826. wb.remove(wb['Sheet'])
  827. # ===== Products Sheet =====
  828. ws_products = wb.create_sheet("Products", 0)
  829. # Headers
  830. products_headers = [
  831. 'ITEM ID', 'PRODUCT NAME', 'PRODUCT TYPE',
  832. 'Product Short Description', 'Product Long Description', 'image_path'
  833. ]
  834. header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
  835. header_font = Font(bold=True, color="FFFFFF")
  836. for col_num, header in enumerate(products_headers, 1):
  837. cell = ws_products.cell(row=1, column=col_num)
  838. cell.value = header
  839. cell.fill = header_fill
  840. cell.font = header_font
  841. cell.alignment = Alignment(horizontal="center", vertical="center")
  842. # Fetch and add product data
  843. products = Product.objects.all()
  844. for row_num, product in enumerate(products, 2):
  845. ws_products.cell(row=row_num, column=1, value=product.item_id)
  846. ws_products.cell(row=row_num, column=2, value=product.product_name)
  847. ws_products.cell(row=row_num, column=3, value=product.product_type)
  848. ws_products.cell(row=row_num, column=4, value=product.product_short_description)
  849. ws_products.cell(row=row_num, column=5, value=product.product_long_description)
  850. ws_products.cell(row=row_num, column=6, value=product.image_path)
  851. # Adjust widths
  852. ws_products.column_dimensions['A'].width = 15
  853. ws_products.column_dimensions['B'].width = 25
  854. ws_products.column_dimensions['C'].width = 15
  855. ws_products.column_dimensions['D'].width = 35
  856. ws_products.column_dimensions['E'].width = 50
  857. ws_products.column_dimensions['F'].width = 45
  858. # ===== Attribute_values Sheet =====
  859. ws_attributes = wb.create_sheet("Attribute_values", 1)
  860. attributes_headers = ['item_id', 'attribute_name', 'original_value']
  861. for col_num, header in enumerate(attributes_headers, 1):
  862. cell = ws_attributes.cell(row=1, column=col_num)
  863. cell.value = header
  864. cell.fill = header_fill
  865. cell.font = header_font
  866. cell.alignment = Alignment(horizontal="center", vertical="center")
  867. # Fetch and add attribute values
  868. attributes = ProductAttributeValue.objects.select_related('product').all()
  869. for row_num, attr in enumerate(attributes, 2):
  870. ws_attributes.cell(row=row_num, column=1, value=attr.product.item_id)
  871. ws_attributes.cell(row=row_num, column=2, value=attr.attribute_name)
  872. ws_attributes.cell(row=row_num, column=3, value=attr.original_value)
  873. ws_attributes.column_dimensions['A'].width = 15
  874. ws_attributes.column_dimensions['B'].width = 25
  875. ws_attributes.column_dimensions['C'].width = 30
  876. # Save to BytesIO
  877. output = io.BytesIO()
  878. wb.save(output)
  879. output.seek(0)
  880. response = HttpResponse(
  881. output.getvalue(),
  882. content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  883. )
  884. response['Content-Disposition'] = 'attachment; filename=products_export.xlsx'
  885. return response
  886. class ProductAttributesUploadView(APIView):
  887. """
  888. POST API to upload an Excel file and add mandatory/additional attributes
  889. for product types with possible values.
  890. """
  891. parser_classes = (MultiPartParser, FormParser)
  892. def post(self, request):
  893. file_obj = request.FILES.get('file')
  894. if not file_obj:
  895. return Response({"error": "No file provided."}, status=status.HTTP_400_BAD_REQUEST)
  896. try:
  897. df = pd.read_excel(file_obj)
  898. required_columns = {'product_type', 'attribute_name', 'is_mandatory', 'possible_values'}
  899. if not required_columns.issubset(df.columns):
  900. return Response({
  901. "error": f"Missing required columns. Found: {list(df.columns)}"
  902. }, status=status.HTTP_400_BAD_REQUEST)
  903. for _, row in df.iterrows():
  904. product_type_name = str(row['product_type']).strip()
  905. attr_name = str(row['attribute_name']).strip()
  906. is_mandatory = str(row['is_mandatory']).strip().lower() in ['yes', 'true', '1']
  907. possible_values = str(row.get('possible_values', '')).strip()
  908. # Get or create product type
  909. product_type, _ = ProductType.objects.get_or_create(name=product_type_name)
  910. # Get or create attribute
  911. attribute, _ = ProductAttribute.objects.get_or_create(
  912. product_type=product_type,
  913. name=attr_name,
  914. defaults={'is_mandatory': is_mandatory}
  915. )
  916. attribute.is_mandatory = is_mandatory
  917. attribute.save()
  918. # Handle possible values
  919. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  920. if possible_values:
  921. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  922. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  923. return Response({"message": "Attributes uploaded successfully."}, status=status.HTTP_201_CREATED)
  924. except Exception as e:
  925. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  926. class ProductTypeAttributesView(APIView):
  927. """
  928. API to view, create, update, and delete product type attributes and their possible values.
  929. Also supports dynamic product type creation.
  930. """
  931. def get(self, request):
  932. """
  933. Retrieve all product types with their attributes and possible values.
  934. """
  935. product_types = ProductType.objects.all()
  936. serializer = ProductTypeSerializer(product_types, many=True)
  937. # Transform the serialized data into the requested format
  938. result = []
  939. for pt in serializer.data:
  940. for attr in pt['attributes']:
  941. result.append({
  942. 'product_type': pt['name'],
  943. 'attribute_name': attr['name'],
  944. 'is_mandatory': 'Yes' if attr['is_mandatory'] else 'No',
  945. 'possible_values': ', '.join([pv['value'] for pv in attr['possible_values']])
  946. })
  947. return Response(result, status=status.HTTP_200_OK)
  948. def post(self, request):
  949. """
  950. Create a new product type or attribute with possible values.
  951. Expected payload example:
  952. {
  953. "product_type": "Hardware Screws",
  954. "attribute_name": "Material",
  955. "is_mandatory": "Yes",
  956. "possible_values": "Steel, Zinc Plated, Stainless Steel"
  957. }
  958. """
  959. try:
  960. product_type_name = request.data.get('product_type')
  961. attribute_name = request.data.get('attribute_name', '')
  962. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  963. possible_values = request.data.get('possible_values', '')
  964. if not product_type_name:
  965. return Response({
  966. "error": "product_type is required"
  967. }, status=status.HTTP_400_BAD_REQUEST)
  968. with transaction.atomic():
  969. # Get or create product type
  970. product_type, created = ProductType.objects.get_or_create(name=product_type_name)
  971. if created and not attribute_name:
  972. return Response({
  973. "message": f"Product type '{product_type_name}' created successfully",
  974. "data": {"product_type": product_type_name}
  975. }, status=status.HTTP_201_CREATED)
  976. if attribute_name:
  977. # Create attribute
  978. attribute, attr_created = ProductAttribute.objects.get_or_create(
  979. product_type=product_type,
  980. name=attribute_name,
  981. defaults={'is_mandatory': is_mandatory}
  982. )
  983. if not attr_created:
  984. return Response({
  985. "error": f"Attribute '{attribute_name}' already exists for product type '{product_type_name}'"
  986. }, status=status.HTTP_400_BAD_REQUEST)
  987. # Handle possible values
  988. if possible_values:
  989. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  990. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  991. return Response({
  992. "message": "Attribute created successfully",
  993. "data": {
  994. "product_type": product_type_name,
  995. "attribute_name": attribute_name,
  996. "is_mandatory": "Yes" if is_mandatory else "No",
  997. "possible_values": possible_values
  998. }
  999. }, status=status.HTTP_201_CREATED)
  1000. return Response({
  1001. "message": f"Product type '{product_type_name}' already exists",
  1002. "data": {"product_type": product_type_name}
  1003. }, status=status.HTTP_200_OK)
  1004. except Exception as e:
  1005. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1006. def put(self, request):
  1007. """
  1008. Update an existing product type attribute and its possible values.
  1009. Expected payload example:
  1010. {
  1011. "product_type": "Hardware Screws",
  1012. "attribute_name": "Material",
  1013. "is_mandatory": "Yes",
  1014. "possible_values": "Steel, Zinc Plated, Stainless Steel, Brass"
  1015. }
  1016. """
  1017. try:
  1018. product_type_name = request.data.get('product_type')
  1019. attribute_name = request.data.get('attribute_name')
  1020. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  1021. possible_values = request.data.get('possible_values', '')
  1022. if not all([product_type_name, attribute_name]):
  1023. return Response({
  1024. "error": "product_type and attribute_name are required"
  1025. }, status=status.HTTP_400_BAD_REQUEST)
  1026. with transaction.atomic():
  1027. try:
  1028. product_type = ProductType.objects.get(name=product_type_name)
  1029. attribute = ProductAttribute.objects.get(
  1030. product_type=product_type,
  1031. name=attribute_name
  1032. )
  1033. except ProductType.DoesNotExist:
  1034. return Response({
  1035. "error": f"Product type '{product_type_name}' not found"
  1036. }, status=status.HTTP_404_NOT_FOUND)
  1037. except ProductAttribute.DoesNotExist:
  1038. return Response({
  1039. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  1040. }, status=status.HTTP_404_NOT_FOUND)
  1041. # Update attribute
  1042. attribute.is_mandatory = is_mandatory
  1043. attribute.save()
  1044. # Update possible values
  1045. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  1046. if possible_values:
  1047. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  1048. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  1049. return Response({
  1050. "message": "Attribute updated successfully",
  1051. "data": {
  1052. "product_type": product_type_name,
  1053. "attribute_name": attribute_name,
  1054. "is_mandatory": "Yes" if is_mandatory else "No",
  1055. "possible_values": possible_values
  1056. }
  1057. }, status=status.HTTP_200_OK)
  1058. except Exception as e:
  1059. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1060. def delete(self, request):
  1061. """
  1062. Delete a product type or a specific attribute.
  1063. Expected payload example:
  1064. {
  1065. "product_type": "Hardware Screws",
  1066. "attribute_name": "Material"
  1067. }
  1068. """
  1069. try:
  1070. product_type_name = request.data.get('product_type')
  1071. attribute_name = request.data.get('attribute_name', '')
  1072. if not product_type_name:
  1073. return Response({
  1074. "error": "product_type is required"
  1075. }, status=status.HTTP_400_BAD_REQUEST)
  1076. with transaction.atomic():
  1077. try:
  1078. product_type = ProductType.objects.get(name=product_type_name)
  1079. except ProductType.DoesNotExist:
  1080. return Response({
  1081. "error": f"Product type '{product_type_name}' not found"
  1082. }, status=status.HTTP_404_NOT_FOUND)
  1083. if attribute_name:
  1084. # Delete specific attribute
  1085. try:
  1086. attribute = ProductAttribute.objects.get(
  1087. product_type=product_type,
  1088. name=attribute_name
  1089. )
  1090. attribute.delete()
  1091. return Response({
  1092. "message": f"Attribute '{attribute_name}' deleted successfully from product type '{product_type_name}'"
  1093. }, status=status.HTTP_200_OK)
  1094. except ProductAttribute.DoesNotExist:
  1095. return Response({
  1096. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  1097. }, status=status.HTTP_404_NOT_FOUND)
  1098. else:
  1099. # Delete entire product type
  1100. product_type.delete()
  1101. return Response({
  1102. "message": f"Product type '{product_type_name}' and all its attributes deleted successfully"
  1103. }, status=status.HTTP_200_OK)
  1104. except Exception as e:
  1105. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1106. class ProductTypeListView(APIView):
  1107. """
  1108. GET API to list all product types (only names).
  1109. """
  1110. def get(self, request):
  1111. product_types = ProductType.objects.values_list('name', flat=True)
  1112. return Response({"product_types": list(product_types)}, status=status.HTTP_200_OK)
  1113. # Add these views to your views.py
  1114. from rest_framework.views import APIView
  1115. from rest_framework.response import Response
  1116. from rest_framework import status
  1117. from rest_framework.parsers import MultiPartParser, FormParser
  1118. from django.db import transaction
  1119. import pandas as pd
  1120. from .models import Product, ProductAttributeValue
  1121. from .serializers import (
  1122. ProductAttributeValueSerializer,
  1123. ProductAttributeValueInputSerializer,
  1124. BulkProductAttributeValueSerializer,
  1125. ProductWithAttributesSerializer
  1126. )
  1127. class ProductAttributeValueView(APIView):
  1128. """
  1129. API to manage manually entered original attribute values.
  1130. GET: Retrieve all attribute values for a product
  1131. POST: Create or update attribute values for a product
  1132. DELETE: Delete attribute values
  1133. """
  1134. def get(self, request):
  1135. """
  1136. Get original attribute values for a specific product or all products.
  1137. Query params: item_id (optional)
  1138. """
  1139. item_id = request.query_params.get('item_id')
  1140. if item_id:
  1141. try:
  1142. product = Product.objects.get(item_id=item_id)
  1143. values = ProductAttributeValue.objects.filter(product=product)
  1144. serializer = ProductAttributeValueSerializer(values, many=True)
  1145. return Response({
  1146. "item_id": item_id,
  1147. "attributes": serializer.data
  1148. }, status=status.HTTP_200_OK)
  1149. except Product.DoesNotExist:
  1150. return Response({
  1151. "error": f"Product with item_id '{item_id}' not found"
  1152. }, status=status.HTTP_404_NOT_FOUND)
  1153. else:
  1154. # Return all attribute values grouped by product
  1155. values = ProductAttributeValue.objects.all().select_related('product')
  1156. serializer = ProductAttributeValueSerializer(values, many=True)
  1157. return Response(serializer.data, status=status.HTTP_200_OK)
  1158. def post(self, request):
  1159. """
  1160. Create or update original attribute value for a product.
  1161. Expected payload:
  1162. {
  1163. "item_id": "3217373735",
  1164. "attribute_name": "Clothing Neck Style",
  1165. "original_value": "V-Neck Square"
  1166. }
  1167. """
  1168. serializer = ProductAttributeValueInputSerializer(data=request.data)
  1169. if not serializer.is_valid():
  1170. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  1171. validated_data = serializer.validated_data
  1172. item_id = validated_data['item_id']
  1173. attribute_name = validated_data['attribute_name']
  1174. original_value = validated_data['original_value']
  1175. try:
  1176. product = Product.objects.get(item_id=item_id)
  1177. except Product.DoesNotExist:
  1178. return Response({
  1179. "error": f"Product with item_id '{item_id}' not found"
  1180. }, status=status.HTTP_404_NOT_FOUND)
  1181. # Create or update the attribute value
  1182. attr_value, created = ProductAttributeValue.objects.update_or_create(
  1183. product=product,
  1184. attribute_name=attribute_name,
  1185. defaults={'original_value': original_value}
  1186. )
  1187. response_serializer = ProductAttributeValueSerializer(attr_value)
  1188. return Response({
  1189. "message": "Attribute value created" if created else "Attribute value updated",
  1190. "data": response_serializer.data
  1191. }, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
  1192. def delete(self, request):
  1193. """
  1194. Delete original attribute value(s).
  1195. Expected payload:
  1196. {
  1197. "item_id": "3217373735",
  1198. "attribute_name": "Clothing Neck Style" # Optional, if not provided deletes all for product
  1199. }
  1200. """
  1201. item_id = request.data.get('item_id')
  1202. attribute_name = request.data.get('attribute_name')
  1203. if not item_id:
  1204. return Response({
  1205. "error": "item_id is required"
  1206. }, status=status.HTTP_400_BAD_REQUEST)
  1207. try:
  1208. product = Product.objects.get(item_id=item_id)
  1209. except Product.DoesNotExist:
  1210. return Response({
  1211. "error": f"Product with item_id '{item_id}' not found"
  1212. }, status=status.HTTP_404_NOT_FOUND)
  1213. if attribute_name:
  1214. # Delete specific attribute
  1215. deleted_count, _ = ProductAttributeValue.objects.filter(
  1216. product=product,
  1217. attribute_name=attribute_name
  1218. ).delete()
  1219. if deleted_count == 0:
  1220. return Response({
  1221. "error": f"Attribute '{attribute_name}' not found for product '{item_id}'"
  1222. }, status=status.HTTP_404_NOT_FOUND)
  1223. return Response({
  1224. "message": f"Attribute '{attribute_name}' deleted successfully"
  1225. }, status=status.HTTP_200_OK)
  1226. else:
  1227. # Delete all attributes for product
  1228. deleted_count, _ = ProductAttributeValue.objects.filter(product=product).delete()
  1229. return Response({
  1230. "message": f"Deleted {deleted_count} attribute(s) for product '{item_id}'"
  1231. }, status=status.HTTP_200_OK)
  1232. class BulkProductAttributeValueView(APIView):
  1233. """
  1234. API for bulk operations on original attribute values.
  1235. POST: Create/update multiple attribute values at once
  1236. """
  1237. def post(self, request):
  1238. """
  1239. Bulk create or update attribute values for multiple products.
  1240. Expected payload:
  1241. {
  1242. "products": [
  1243. {
  1244. "item_id": "3217373735",
  1245. "attributes": {
  1246. "Clothing Neck Style": "V-Neck Square",
  1247. "Condition": "New with tags"
  1248. }
  1249. },
  1250. {
  1251. "item_id": "1234567890",
  1252. "attributes": {
  1253. "Material": "Cotton",
  1254. "Size": "L"
  1255. }
  1256. }
  1257. ]
  1258. }
  1259. """
  1260. products_data = request.data.get('products', [])
  1261. if not products_data:
  1262. return Response({
  1263. "error": "products list is required"
  1264. }, status=status.HTTP_400_BAD_REQUEST)
  1265. results = []
  1266. successful = 0
  1267. failed = 0
  1268. with transaction.atomic():
  1269. for product_data in products_data:
  1270. serializer = BulkProductAttributeValueSerializer(data=product_data)
  1271. if not serializer.is_valid():
  1272. failed += 1
  1273. results.append({
  1274. "item_id": product_data.get('item_id'),
  1275. "status": "failed",
  1276. "error": serializer.errors
  1277. })
  1278. continue
  1279. validated_data = serializer.validated_data
  1280. item_id = validated_data['item_id']
  1281. attributes = validated_data['attributes']
  1282. try:
  1283. product = Product.objects.get(item_id=item_id)
  1284. created_count = 0
  1285. updated_count = 0
  1286. for attr_name, original_value in attributes.items():
  1287. _, created = ProductAttributeValue.objects.update_or_create(
  1288. product=product,
  1289. attribute_name=attr_name,
  1290. defaults={'original_value': original_value}
  1291. )
  1292. if created:
  1293. created_count += 1
  1294. else:
  1295. updated_count += 1
  1296. successful += 1
  1297. results.append({
  1298. "item_id": item_id,
  1299. "status": "success",
  1300. "created": created_count,
  1301. "updated": updated_count
  1302. })
  1303. except Product.DoesNotExist:
  1304. failed += 1
  1305. results.append({
  1306. "item_id": item_id,
  1307. "status": "failed",
  1308. "error": f"Product not found"
  1309. })
  1310. return Response({
  1311. "results": results,
  1312. "total_products": len(products_data),
  1313. "successful": successful,
  1314. "failed": failed
  1315. }, status=status.HTTP_200_OK)
  1316. class ProductListWithAttributesView(APIView):
  1317. """
  1318. GET API to list all products with their original attribute values.
  1319. """
  1320. def get(self, request):
  1321. item_id = request.query_params.get('item_id')
  1322. if item_id:
  1323. try:
  1324. product = Product.objects.get(item_id=item_id)
  1325. serializer = ProductWithAttributesSerializer(product)
  1326. return Response(serializer.data, status=status.HTTP_200_OK)
  1327. except Product.DoesNotExist:
  1328. return Response({
  1329. "error": f"Product with item_id '{item_id}' not found"
  1330. }, status=status.HTTP_404_NOT_FOUND)
  1331. else:
  1332. products = Product.objects.all()
  1333. serializer = ProductWithAttributesSerializer(products, many=True)
  1334. return Response(serializer.data, status=status.HTTP_200_OK)