Merge branch 'development' into diego
This commit is contained in:
43
README.md
43
README.md
@@ -8,11 +8,14 @@ This README aims to document functionality of backend as well as required steps
|
|||||||
- [Load location data](#load-location-data)
|
- [Load location data](#load-location-data)
|
||||||
- [Load taxonomy data](#load-taxonomy-data)
|
- [Load taxonomy data](#load-taxonomy-data)
|
||||||
- [Endpoints](#endpoints)
|
- [Endpoints](#endpoints)
|
||||||
|
- [Shop Integrations](#shop-integrations)
|
||||||
|
- [WooCommerce](#woocommerce)
|
||||||
- [Product Search](#product-search)
|
- [Product Search](#product-search)
|
||||||
- [Massive Data Load Endpoints](#massive-data-load-endpoints)
|
- [Massive Data Load Endpoints](#massive-data-load-endpoints)
|
||||||
- [COOP and Managing User Data Load](#coop-and-managing-user-data-load)
|
- [COOP and Managing User Data Load](#coop-and-managing-user-data-load)
|
||||||
- [Product Data Load](#product-data-load)
|
- [Product Data Load](#product-data-load)
|
||||||
- [GeoIP Setup](#geoip-setup)
|
- [GeoIP Setup](#geoip-setup)
|
||||||
|
- [Tags](#tags)
|
||||||
- [Development Utils](#development-utils)
|
- [Development Utils](#development-utils)
|
||||||
- [Fake product data generation](#fake-product-data-generation)
|
- [Fake product data generation](#fake-product-data-generation)
|
||||||
|
|
||||||
@@ -169,6 +172,30 @@ Location ednpoints:
|
|||||||
- `/api/v1/cities/`
|
- `/api/v1/cities/`
|
||||||
|
|
||||||
|
|
||||||
|
## Shop Integrations
|
||||||
|
|
||||||
|
We provide integrations with online shop platforms
|
||||||
|
|
||||||
|
It requires the json field `Company.credentials` to have the appropiate format and values
|
||||||
|
|
||||||
|
Endoint: `/api/v1/companies/{PK}/import_products/`
|
||||||
|
|
||||||
|
The software to handle different platform imports can be found in `utils`
|
||||||
|
|
||||||
|
### WooCommerce
|
||||||
|
|
||||||
|
Credential format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "qwerweqr",
|
||||||
|
"secret": "asdfsa",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Method: `utils.woocommerce.migrate_shop_products`
|
||||||
|
|
||||||
|
|
||||||
## Product Search
|
## Product Search
|
||||||
|
|
||||||
Endpoint: `/api/v1/product_search/`
|
Endpoint: `/api/v1/product_search/`
|
||||||
@@ -231,6 +258,22 @@ Optional:
|
|||||||
`sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin`
|
`sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin`
|
||||||
|
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
Both `Company` and `Product` models make use of tags.
|
||||||
|
|
||||||
|
### Load shopping taxonomy
|
||||||
|
|
||||||
|
To create the initial set of tags, we can use the `addtaxonomy` management command.
|
||||||
|
Reads the data from `datasets/shop-taxonomy.es-ES.txt` which is from google shopping
|
||||||
|
|
||||||
|
|
||||||
|
### Top-level tags
|
||||||
|
|
||||||
|
In order to extract the top level tags for use as categories, we can use the `extractparenttas` management command.
|
||||||
|
It saves the results to `datasets/top_tags.txt`
|
||||||
|
|
||||||
|
|
||||||
## Development Utils
|
## Development Utils
|
||||||
|
|
||||||
### Fake product data generation
|
### Fake product data generation
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
# from django.db import models
|
# from django.db import models
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
|
from django.contrib.postgres.fields import JSONField
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from tagulous.models import TagField
|
from tagulous.models import TagField
|
||||||
# from core.models import TreeTag
|
|
||||||
|
|
||||||
|
|
||||||
|
from history.models import HistorySync
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
|
# User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class Company(models.Model):
|
class Company(models.Model):
|
||||||
|
|
||||||
WOO_COMMERCE = 'WOO_COMMERCE'
|
WOO_COMMERCE = 'WOO_COMMERCE'
|
||||||
@@ -42,14 +45,13 @@ class Company(models.Model):
|
|||||||
sync = models.BooleanField('Sincronizar tienda', default=False, null=True, blank=True)
|
sync = models.BooleanField('Sincronizar tienda', default=False, null=True, blank=True)
|
||||||
is_validated = models.BooleanField('Validado', default=False, null=True, blank=True)
|
is_validated = models.BooleanField('Validado', default=False, null=True, blank=True)
|
||||||
is_active = models.BooleanField('Activado', default=False, null=True, blank=True)
|
is_active = models.BooleanField('Activado', default=False, null=True, blank=True)
|
||||||
|
credentials = JSONField(null=True)
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
created = models.DateTimeField('date of creation', auto_now_add=True)
|
created = models.DateTimeField('date of creation', auto_now_add=True)
|
||||||
updated = models.DateTimeField('date last update', auto_now=True)
|
updated = models.DateTimeField('date last update', auto_now=True)
|
||||||
creator = models.ForeignKey('core.CustomUser', on_delete=models.DO_NOTHING, null=True, related_name='creator')
|
creator = models.ForeignKey('core.CustomUser', on_delete=models.DO_NOTHING, null=True, related_name='creator')
|
||||||
|
# history = models.ForeignKey(HistorySync, null=True, on_delete=models.DO_NOTHING, related_name='company')
|
||||||
def __str__(self):
|
|
||||||
return self.company_name
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Compañía"
|
verbose_name = "Compañía"
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from companies.serializers import CompanySerializer
|
|||||||
from utils.tag_filters import CompanyTagFilter
|
from utils.tag_filters import CompanyTagFilter
|
||||||
from back_latienda.permissions import IsCreator
|
from back_latienda.permissions import IsCreator
|
||||||
|
|
||||||
|
from utils import woocommerce
|
||||||
|
|
||||||
|
|
||||||
class CompanyViewSet(viewsets.ModelViewSet):
|
class CompanyViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Company.objects.all()
|
queryset = Company.objects.all()
|
||||||
@@ -35,8 +37,7 @@ class CompanyViewSet(viewsets.ModelViewSet):
|
|||||||
Send email to company.creator
|
Send email to company.creator
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
queryset = self.get_custom_queryset(request)
|
instance = self.queryset.filter(pk=kwargs['pk']).first()
|
||||||
instance = queryset.filter(pk=kwargs['pk']).first()
|
|
||||||
if instance:
|
if instance:
|
||||||
# IP stuff
|
# IP stuff
|
||||||
client_ip, is_routable = get_client_ip(request)
|
client_ip, is_routable = get_client_ip(request)
|
||||||
@@ -121,6 +122,35 @@ class CompanyViewSet(viewsets.ModelViewSet):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response({"errors":{"details": str(e),}}, status=500)
|
return Response({"errors":{"details": str(e),}}, status=500)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['GET', ])
|
||||||
|
def import_products(self, request, **kwargs):
|
||||||
|
instance = self.queryset.filter(pk=kwargs['pk']).first()
|
||||||
|
# check if it's a shop
|
||||||
|
if instance.shop is not True:
|
||||||
|
return Response({'error': 'This company is not a shop'})
|
||||||
|
# check required credentials
|
||||||
|
credentials = instance.credentials
|
||||||
|
if credentials is None or credentials == {}:
|
||||||
|
return Response({'error': 'This company has no registered credentials'})
|
||||||
|
# check what platform
|
||||||
|
platform = instance.platform
|
||||||
|
if platform is None:
|
||||||
|
message = {'error': 'This company is not registered with any platforms'}
|
||||||
|
elif platform == 'WOO_COMMERCE':
|
||||||
|
# recheck credentials
|
||||||
|
if 'key' in credentials.keys() and 'secret' in credentials.keys():
|
||||||
|
# execute import
|
||||||
|
products = woocommerce.migrate_shop_products(
|
||||||
|
instance.web_link,
|
||||||
|
credentials['key'],
|
||||||
|
credentials['secret'])
|
||||||
|
message = {'details': f'{len(products)} products added for {instance.company_name}'}
|
||||||
|
else:
|
||||||
|
message = {"error": 'Credentials have wrong format'}
|
||||||
|
else:
|
||||||
|
message = {'error': f'Platform {plaform} not registered'}
|
||||||
|
return Response(message)
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET',])
|
@api_view(['GET',])
|
||||||
@permission_classes([IsAuthenticated,])
|
@permission_classes([IsAuthenticated,])
|
||||||
|
|||||||
31
core/management/commands/extractparenttags.py
Normal file
31
core/management/commands/extractparenttags.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from core.models import TreeTag
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
help = 'Extract top level tags'
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
# get all instances
|
||||||
|
tags = TreeTag.objects.all()
|
||||||
|
top_tags = []
|
||||||
|
|
||||||
|
print("Extracting top-level tags from TreeTag instances")
|
||||||
|
# extract tags with no ancestor
|
||||||
|
for tag in tags:
|
||||||
|
if not tag.get_ancestors():
|
||||||
|
top_tags.append(tag.name)
|
||||||
|
|
||||||
|
print("Saving top-level tags to file")
|
||||||
|
# save results to dataset/top_tags.txt
|
||||||
|
path = f"{settings.BASE_DIR}/../datasets/top_tags.txt"
|
||||||
|
with open(path, 'wt') as f:
|
||||||
|
f.writelines([tag + '\n' for tag in top_tags])
|
||||||
|
|
||||||
|
# print out results
|
||||||
|
logging.info(f"Extracted {len(top_tags)} to {path}")
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
# Google_Product_Taxonomy_Version: 2015-02-19
|
|
||||||
Alimentación, bebida y tabaco
|
Alimentación, bebida y tabaco
|
||||||
Alimentación, bebida y tabaco/Alimentos
|
Alimentación, bebida y tabaco/Alimentos
|
||||||
Alimentación, bebida y tabaco/Alimentos/Aliños y especias
|
Alimentación, bebida y tabaco/Alimentos/Aliños y especias
|
||||||
@@ -3786,7 +3785,7 @@ Equipamiento deportivo/Equipamiento para atletismo/Gimnasia/Trampolines
|
|||||||
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse
|
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse
|
||||||
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Conjuntos de equipamiento para lacrosse
|
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Conjuntos de equipamiento para lacrosse
|
||||||
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse
|
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse
|
||||||
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Almohadillas de hombros para lacrosse y hockey sobre hierba
|
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Almohadillas de hombros para lacrosse y hockey sobre hierba
|
||||||
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Cascos para hockey sobre hierba y lacrosse
|
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Cascos para hockey sobre hierba y lacrosse
|
||||||
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Guantes para hockey sobre hierba y lacrosse
|
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Guantes para hockey sobre hierba y lacrosse
|
||||||
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Máscaras y gafas protectoras para hockey sobre hierba y lacrosse
|
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Máscaras y gafas protectoras para hockey sobre hierba y lacrosse
|
||||||
|
|||||||
21
datasets/top_tags.txt
Normal file
21
datasets/top_tags.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Alimentación, bebida y tabaco
|
||||||
|
Arte y ocio
|
||||||
|
Bebés y niños pequeños
|
||||||
|
Bricolaje
|
||||||
|
Cámaras y ópticas
|
||||||
|
Casa y jardín
|
||||||
|
Economía e industria
|
||||||
|
Electrónica
|
||||||
|
Elementos religiosos y ceremoniales
|
||||||
|
Equipamiento deportivo
|
||||||
|
Juegos y juguetes
|
||||||
|
Maletas y bolsos de viaje
|
||||||
|
Material de oficina
|
||||||
|
Mobiliario
|
||||||
|
Multimedia
|
||||||
|
Productos para adultos
|
||||||
|
Productos para mascotas y animales
|
||||||
|
Ropa y accesorios
|
||||||
|
Salud y belleza
|
||||||
|
Software
|
||||||
|
Vehículos y recambios
|
||||||
@@ -35,7 +35,7 @@ class Product(models.Model):
|
|||||||
discount = models.DecimalField('Descuento', max_digits=5, decimal_places=2, null=True, blank=True)
|
discount = models.DecimalField('Descuento', max_digits=5, decimal_places=2, null=True, blank=True)
|
||||||
stock = models.PositiveIntegerField('Stock', null=True)
|
stock = models.PositiveIntegerField('Stock', null=True)
|
||||||
tags = TagField(to=TreeTag)
|
tags = TagField(to=TreeTag)
|
||||||
category = SingleTagField(null=True) # main tag category
|
category = SingleTagField(blank=True, null=True) # main tag category
|
||||||
attributes = TagField(to=TreeTag, related_name='product_attributes')
|
attributes = TagField(to=TreeTag, related_name='product_attributes')
|
||||||
identifiers = models.TextField('Identificador único de producto', null=True, blank=True)
|
identifiers = models.TextField('Identificador único de producto', null=True, blank=True)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ from utils.tag_serializers import TagListSerializerField, TaggitSerializer, Sing
|
|||||||
|
|
||||||
class ProductSerializer(TaggitSerializer, serializers.ModelSerializer):
|
class ProductSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||||
|
|
||||||
|
tags = TagListSerializerField(required=False)
|
||||||
|
category = SingleTagSerializerField(required=False) # main tag category
|
||||||
|
attributes = TagListSerializerField(required=False)
|
||||||
|
# image = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
exclude = ['created', 'updated', 'creator']
|
||||||
|
|
||||||
|
|
||||||
|
class ProductSearchSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||||
|
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
category = SingleTagSerializerField(required=False) # main tag category
|
category = SingleTagSerializerField(required=False) # main tag category
|
||||||
attributes = TagListSerializerField(required=False)
|
attributes = TagListSerializerField(required=False)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from rest_framework.decorators import api_view, permission_classes, action
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from products.models import Product
|
from products.models import Product
|
||||||
from products.serializers import ProductSerializer, TagFilterSerializer
|
from products.serializers import ProductSerializer, TagFilterSerializer, ProductSearchSerializer
|
||||||
from companies.models import Company
|
from companies.models import Company
|
||||||
from history.models import HistorySync
|
from history.models import HistorySync
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class ProductViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
@action(detail=True, methods=['GET',])
|
@action(detail=True, methods=['GET',])
|
||||||
def related(request):
|
def related(request):
|
||||||
# find the most similar products
|
# TODO: find the most similar products
|
||||||
return Response(data=[])
|
return Response(data=[])
|
||||||
|
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ def product_search(request):
|
|||||||
# extract filters from result_set
|
# extract filters from result_set
|
||||||
filters = extract_search_filters(result_set)
|
filters = extract_search_filters(result_set)
|
||||||
# serialize and respond
|
# serialize and respond
|
||||||
product_serializer = ProductSerializer(result_set, many=True)
|
product_serializer = ProductSearchSerializer(result_set, many=True, context={'request': request})
|
||||||
return Response(data={"filters": filters, "products": product_serializer.data})
|
return Response(data={"filters": filters, "products": product_serializer.data})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response({"errors": {"details": str(type(e))}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({"errors": {"details": str(type(e))}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ django-tagulous==1.1.0
|
|||||||
Pillow==8.1.0
|
Pillow==8.1.0
|
||||||
drf-extra-fields==3.0.4
|
drf-extra-fields==3.0.4
|
||||||
django-ipware==3.0.2
|
django-ipware==3.0.2
|
||||||
geoip2==4.1.0
|
geoip2==4.1.0
|
||||||
|
woocommerce==2.1.1
|
||||||
|
|||||||
115
utils/woocommerce.py
Normal file
115
utils/woocommerce.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
This file holds the functions necesary to:
|
||||||
|
|
||||||
|
- Connect to seller's shop API
|
||||||
|
- Load information on seller [??]
|
||||||
|
- Load product information
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from woocommerce import API
|
||||||
|
|
||||||
|
from companies.models import Company
|
||||||
|
from products.models import Product
|
||||||
|
from products.serializers import ProductSerializer
|
||||||
|
|
||||||
|
|
||||||
|
def get_wcapi_instance(url, key, secret, version="wc/v3"):
|
||||||
|
wcapi = API(
|
||||||
|
url=url,
|
||||||
|
consumer_key=key,
|
||||||
|
consumer_secret=secret,
|
||||||
|
wp_api=True,
|
||||||
|
version=version
|
||||||
|
)
|
||||||
|
return wcapi
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_shop_products(url, key, secret, version="wc/v3"):
|
||||||
|
# get wcapi
|
||||||
|
wcapi = get_wcapi_instance(url, key, secret, version)
|
||||||
|
|
||||||
|
consumer_key = 'ck_565539bb25b472b1ff7a209eb157ca11c0a26397'
|
||||||
|
consumer_secret = 'cs_9c1690ba5da0dd70f51d61c395628fa14d1a104c'
|
||||||
|
|
||||||
|
# get company fom url
|
||||||
|
company = Company.objects.filter(web_link=url).first()
|
||||||
|
|
||||||
|
if not company:
|
||||||
|
# logging.error(f"Could not find Company with URL: {url}")
|
||||||
|
# print(f"Could not find Company with URL: {url}")
|
||||||
|
# return None
|
||||||
|
# TODO: ELIMINATE THIS AFTER DEBUGGING
|
||||||
|
company = Company.objects.create(web_link=url)
|
||||||
|
logging.error(f"Created Company for testing: {url}")
|
||||||
|
|
||||||
|
# list products
|
||||||
|
response = wcapi.get('/products/')
|
||||||
|
if response.status_code == 200:
|
||||||
|
products = response.json()
|
||||||
|
elif response.status_code == 401:
|
||||||
|
logging.error(f"{response.status_code} [{response.url}]: {response.json()}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
logging.error(f"Could not load products from {url}: [{response.status_code}]")
|
||||||
|
print(f"Could not load products fom {url}: [{response.status_code}]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
product_fields = [f.name for f in Product._meta.get_fields()]
|
||||||
|
counter = 0
|
||||||
|
products_created = []
|
||||||
|
for product in products:
|
||||||
|
instance_data = {'company':company.id}
|
||||||
|
# parse the product info
|
||||||
|
for key in product:
|
||||||
|
if key in product_fields:
|
||||||
|
instance_data[key] = product[key]
|
||||||
|
# remove unwanted fields
|
||||||
|
instance_data.pop('id')
|
||||||
|
# extract m2m field data
|
||||||
|
tags = instance_data.pop('tags')
|
||||||
|
attributes = instance_data.pop('attributes')
|
||||||
|
|
||||||
|
# create instance with serializer
|
||||||
|
'''
|
||||||
|
serializer = ProductSerializer(data=instance_data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
new = serializer.save()
|
||||||
|
if tags:
|
||||||
|
new.tags.set(tags)
|
||||||
|
if attributes:
|
||||||
|
new.attributes.set(attributes)
|
||||||
|
new.save()
|
||||||
|
else:
|
||||||
|
logging.error(f"{serializer.errors}")
|
||||||
|
continue
|
||||||
|
'''
|
||||||
|
# alternative method
|
||||||
|
serializer = ProductSerializer(data=instance_data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
try:
|
||||||
|
new = Product.objects.create(**serializer.validated_data)
|
||||||
|
if tags:
|
||||||
|
new.tags.set(tags)
|
||||||
|
if attributes:
|
||||||
|
new.attributes.set(attributes)
|
||||||
|
new.save()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not create product instance: {str(e)}")
|
||||||
|
else:
|
||||||
|
logging.error(f"{serializer.errors}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
products_created.append(new)
|
||||||
|
counter += 1
|
||||||
|
logging.info(f"Product {instance_data.get('name')} created")
|
||||||
|
print(f"Product {instance_data.get('name')} created")
|
||||||
|
|
||||||
|
print(f"Products created: {counter}")
|
||||||
|
return products_created
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user