Merge pull request 'Manejo de productos' (#2) from manejo_productos into main
Reviewed-on: OneTeam/don_confiao#2
This commit is contained in:
		@@ -1,5 +1,7 @@
 | 
				
			|||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
from .models import Sale, SaleLine
 | 
					from .models import Sale, SaleLine, Product, ProductCategory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
admin.site.register(Sale)
 | 
					admin.site.register(Sale)
 | 
				
			||||||
admin.site.register(SaleLine)
 | 
					admin.site.register(SaleLine)
 | 
				
			||||||
 | 
					admin.site.register(Product)
 | 
				
			||||||
 | 
					admin.site.register(ProductCategory)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								tienda_ilusion/don_confiao/example_products.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tienda_ilusion/don_confiao/example_products.csv
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					"producto","unidad","precio","categorias"
 | 
				
			||||||
 | 
					"Aceite","Unidad", 50000,"Aceites&Alimentos"
 | 
				
			||||||
 | 
					"Café","Unidad", 14000,"Cafes&Alimentos"
 | 
				
			||||||
 | 
					"Arroz","Unidad", 7000,"Cafes&Alimentos"
 | 
				
			||||||
		
		
			
  | 
							
								
								
									
										4
									
								
								tienda_ilusion/don_confiao/example_products2.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tienda_ilusion/don_confiao/example_products2.csv
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					"producto","unidad","precio","categorias"
 | 
				
			||||||
 | 
					"Aceite","Unidad", 50000,"Aceites&Alimentos"
 | 
				
			||||||
 | 
					"Café","Unidad", 15000,"Cafes&Alimentos"
 | 
				
			||||||
 | 
					"Arroz","Unidad", 6000,"Alimentos&Granos"
 | 
				
			||||||
		
		
			
  | 
							
								
								
									
										4
									
								
								tienda_ilusion/don_confiao/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tienda_ilusion/don_confiao/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					from django import forms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImportProductsForm(forms.Form):
 | 
				
			||||||
 | 
					    csv_file = forms.FileField()
 | 
				
			||||||
@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.6 on 2024-06-29 18:55
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('don_confiao', '0008_alter_sale_phone_alter_saleline_description'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='ProductCategory',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=100)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Product',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=100)),
 | 
				
			||||||
 | 
					                ('price', models.DecimalField(decimal_places=2, max_digits=9)),
 | 
				
			||||||
 | 
					                ('measuring_unit', models.CharField(choices=[('UNIT', 'Unit')], default='UNIT', max_length=20)),
 | 
				
			||||||
 | 
					                ('categories', models.ManyToManyField(to='don_confiao.productcategory')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.6 on 2024-06-29 21:04
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('don_confiao', '0009_productcategory_product'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='productcategory',
 | 
				
			||||||
 | 
					            name='name',
 | 
				
			||||||
 | 
					            field=models.CharField(max_length=100, unique=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.6 on 2024-06-29 21:48
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('don_confiao', '0010_alter_productcategory_name'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='product',
 | 
				
			||||||
 | 
					            name='name',
 | 
				
			||||||
 | 
					            field=models.CharField(max_length=100, unique=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Sale(models.Model):
 | 
					class Sale(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,3 +16,25 @@ class SaleLine(models.Model):
 | 
				
			|||||||
    quantity = models.IntegerField(null=True)
 | 
					    quantity = models.IntegerField(null=True)
 | 
				
			||||||
    unit_price = models.DecimalField(max_digits=9, decimal_places=2)
 | 
					    unit_price = models.DecimalField(max_digits=9, decimal_places=2)
 | 
				
			||||||
    description = models.CharField(max_length=255, null=True, blank=True)
 | 
					    description = models.CharField(max_length=255, null=True, blank=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MeasuringUnits(models.TextChoices):
 | 
				
			||||||
 | 
					    UNIT = 'UNIT', _('Unit')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ProductCategory(models.Model):
 | 
				
			||||||
 | 
					    name = models.CharField(max_length=100, unique=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Product(models.Model):
 | 
				
			||||||
 | 
					    name = models.CharField(max_length=100, unique=True)
 | 
				
			||||||
 | 
					    price = models.DecimalField(max_digits=9, decimal_places=2)
 | 
				
			||||||
 | 
					    measuring_unit = models.CharField(
 | 
				
			||||||
 | 
					        max_length=20,
 | 
				
			||||||
 | 
					        choices=MeasuringUnits.choices,
 | 
				
			||||||
 | 
					        default=MeasuringUnits.UNIT
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    categories = models.ManyToManyField(ProductCategory)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.name
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					{% if form.is_multipart %}
 | 
				
			||||||
 | 
					    <form enctype="multipart/form-data" method="post">
 | 
				
			||||||
 | 
					{% else %}
 | 
				
			||||||
 | 
					    <form method="post">
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% csrf_token %}
 | 
				
			||||||
 | 
					{{ form }}
 | 
				
			||||||
 | 
					<input type="submit" value="Importar">
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
@@ -2,5 +2,6 @@
 | 
				
			|||||||
<h2>Don Confiao</h2>
 | 
					<h2>Don Confiao</h2>
 | 
				
			||||||
<ul>
 | 
					<ul>
 | 
				
			||||||
    <li><a href='./comprar'>Comprar</a></li>
 | 
					    <li><a href='./comprar'>Comprar</a></li>
 | 
				
			||||||
    <li><a href='./compras'>Compras</a></li>
 | 
					    <li><a href='./productos'>Productos</a></li>
 | 
				
			||||||
 | 
					    <li><a href='./importar_productos'>Importar Productos</a></li>
 | 
				
			||||||
</ul>
 | 
					</ul>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								tienda_ilusion/don_confiao/test_products.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								tienda_ilusion/don_confiao/test_products.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					from django.test import Client, TestCase
 | 
				
			||||||
 | 
					from django.contrib.auth.models import AnonymousUser, User
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .views import import_products, products
 | 
				
			||||||
 | 
					from .models import ProductCategory, Product
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestProducts(TestCase):
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.client = Client()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_import_products(self):
 | 
				
			||||||
 | 
					        self._import_csv()
 | 
				
			||||||
 | 
					        all_products = self._get_products()
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            len(all_products),
 | 
				
			||||||
 | 
					            3
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_import_products_with_categories(self):
 | 
				
			||||||
 | 
					        self._import_csv()
 | 
				
			||||||
 | 
					        all_products = self._get_products()
 | 
				
			||||||
 | 
					        self.assertIn("Aceites", all_products[0]["categories"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_don_repeat_categories_on_import(self):
 | 
				
			||||||
 | 
					        self._import_csv()
 | 
				
			||||||
 | 
					        categories_on_csv = ["Cafes", "Alimentos", "Aceites"]
 | 
				
			||||||
 | 
					        categories = ProductCategory.objects.all()
 | 
				
			||||||
 | 
					        self.assertCountEqual(
 | 
				
			||||||
 | 
					            [c.name for c in categories],
 | 
				
			||||||
 | 
					            categories_on_csv
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_update_products(self):
 | 
				
			||||||
 | 
					        self._import_csv()
 | 
				
			||||||
 | 
					        first_products = self._get_products()
 | 
				
			||||||
 | 
					        self._import_csv('example_products2.csv')
 | 
				
			||||||
 | 
					        seconds_products = self._get_products()
 | 
				
			||||||
 | 
					        self.assertEqual(len(first_products), len(seconds_products))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_preserve_id_on_import(self):
 | 
				
			||||||
 | 
					        self._import_csv()
 | 
				
			||||||
 | 
					        id_aceite = Product.objects.get(name='Aceite').id
 | 
				
			||||||
 | 
					        self._import_csv('example_products2.csv')
 | 
				
			||||||
 | 
					        id_post_updated = Product.objects.get(name='Aceite').id
 | 
				
			||||||
 | 
					        self.assertEqual(id_aceite, id_post_updated)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_update_categories_on_import(self):
 | 
				
			||||||
 | 
					        self._import_csv()
 | 
				
			||||||
 | 
					        first_products = self._get_products()
 | 
				
			||||||
 | 
					        first_categories = {p["name"]: p["categories"] for p in first_products}
 | 
				
			||||||
 | 
					        self._import_csv('example_products2.csv')
 | 
				
			||||||
 | 
					        updated_products = self._get_products()
 | 
				
			||||||
 | 
					        updated_categories = {p["name"]: p["categories"] for p in updated_products}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('Cafes', first_categories['Arroz'])
 | 
				
			||||||
 | 
					        self.assertNotIn('Granos', first_categories['Arroz'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('Granos', updated_categories['Arroz'])
 | 
				
			||||||
 | 
					        self.assertNotIn('Cafes', updated_categories['Arroz'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_update_price(self):
 | 
				
			||||||
 | 
					        self._import_csv()
 | 
				
			||||||
 | 
					        first_products = self._get_products()
 | 
				
			||||||
 | 
					        first_prices = {p["name"]: p["price_list"] for p in first_products}
 | 
				
			||||||
 | 
					        expected_first_prices = {
 | 
				
			||||||
 | 
					            "Aceite": '50000.00',
 | 
				
			||||||
 | 
					            "Café": '14000.00',
 | 
				
			||||||
 | 
					            "Arroz": '7000.00'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self.assertDictEqual(
 | 
				
			||||||
 | 
					            expected_first_prices,
 | 
				
			||||||
 | 
					            first_prices
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._import_csv('example_products2.csv')
 | 
				
			||||||
 | 
					        updated_products = self._get_products()
 | 
				
			||||||
 | 
					        updated_prices = {p["name"]: p["price_list"] for p in updated_products}
 | 
				
			||||||
 | 
					        expected_updated_prices = {
 | 
				
			||||||
 | 
					            "Aceite": '50000.00',
 | 
				
			||||||
 | 
					            "Café": '15000.00',
 | 
				
			||||||
 | 
					            "Arroz": '6000.00'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertDictEqual(
 | 
				
			||||||
 | 
					            expected_updated_prices,
 | 
				
			||||||
 | 
					            updated_prices
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_products(self):
 | 
				
			||||||
 | 
					        products_response = self.client.get("/don_confiao/productos")
 | 
				
			||||||
 | 
					        return json.loads(products_response.content.decode('utf-8'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _import_csv(self, csv_file='example_products.csv'):
 | 
				
			||||||
 | 
					        app_name = "don_confiao"
 | 
				
			||||||
 | 
					        app_dir = os.path.join(settings.BASE_DIR, app_name)
 | 
				
			||||||
 | 
					        example_csv = os.path.join(app_dir, csv_file)
 | 
				
			||||||
 | 
					        with open(example_csv, 'rb') as csv:
 | 
				
			||||||
 | 
					            self.client.post(
 | 
				
			||||||
 | 
					                "/don_confiao/importar_productos",
 | 
				
			||||||
 | 
					                {"csv_file": csv}
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
@@ -2,9 +2,11 @@ from django.urls import path
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from . import views
 | 
					from . import views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app_name = 'don_confiao'
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("", views.index, name="wellcome"),
 | 
					    path("", views.index, name="wellcome"),
 | 
				
			||||||
    path("comprar", views.buy, name="buy"),
 | 
					    path("comprar", views.buy, name="buy"),
 | 
				
			||||||
    path("compras", views.purchases, name="purchases"),
 | 
					    path("compras", views.purchases, name="purchases"),
 | 
				
			||||||
    path("productos", views.products, name="products"),
 | 
					    path("productos", views.products, name="products"),
 | 
				
			||||||
 | 
					    path("importar_productos", views.import_products, name="import_products")
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
from django.shortcuts import render
 | 
					from django.shortcuts import render
 | 
				
			||||||
from django.http import HttpResponse, JsonResponse
 | 
					from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
 | 
				
			||||||
from django.template import loader
 | 
					from django.template import loader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Sale
 | 
					from .models import Sale, Product, ProductCategory
 | 
				
			||||||
 | 
					from .forms import ImportProductsForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import csv
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def index(request):
 | 
					def index(request):
 | 
				
			||||||
    return render(request, 'don_confiao/index.html')
 | 
					    return render(request, 'don_confiao/index.html')
 | 
				
			||||||
@@ -22,18 +25,53 @@ def purchases(request):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def products(request):
 | 
					def products(request):
 | 
				
			||||||
    products = [
 | 
					    rproducts = []
 | 
				
			||||||
        {
 | 
					    products = Product.objects.all()
 | 
				
			||||||
            "name": "Aceite de Coco Artesanal 500ml",
 | 
					    for product in products:
 | 
				
			||||||
            "price_list": 50000,
 | 
					        rproduct = {
 | 
				
			||||||
            "uom": "Unit",
 | 
					            "name": product.name,
 | 
				
			||||||
            "category": "Aceites"
 | 
					            "price_list": product.price,
 | 
				
			||||||
        },
 | 
					            "uom": product.measuring_unit,
 | 
				
			||||||
        {
 | 
					            "categories": [c.name for c in product.categories.all()]
 | 
				
			||||||
            "name": "Cafe 500ml",
 | 
					        }
 | 
				
			||||||
            "price_list": 14000,
 | 
					        rproducts.append(rproduct)
 | 
				
			||||||
            "uom": "Unit",
 | 
					
 | 
				
			||||||
            "category": "Cafes"
 | 
					    return JsonResponse(rproducts, safe=False)
 | 
				
			||||||
        },
 | 
					
 | 
				
			||||||
    ]
 | 
					def import_products(request):
 | 
				
			||||||
    return JsonResponse(products, safe=False)
 | 
					    if request.method == "POST":
 | 
				
			||||||
 | 
					        form = ImportProductsForm(request.POST, request.FILES)
 | 
				
			||||||
 | 
					        if form.is_valid():
 | 
				
			||||||
 | 
					            handle_import_products_file(request.FILES["csv_file"])
 | 
				
			||||||
 | 
					            return HttpResponseRedirect("productos")
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = ImportProductsForm()
 | 
				
			||||||
 | 
					    return render(
 | 
				
			||||||
 | 
					        request,
 | 
				
			||||||
 | 
					        "don_confiao/import_products.html",
 | 
				
			||||||
 | 
					        {'form': form}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _categories_from_csv_string(categories_string, separator="&"):
 | 
				
			||||||
 | 
					    categories = categories_string.split(separator)
 | 
				
			||||||
 | 
					    clean_categories = [c.strip() for c in categories]
 | 
				
			||||||
 | 
					    return [_category_from_name(category) for category in clean_categories]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _category_from_name(name):
 | 
				
			||||||
 | 
					    return ProductCategory.objects.get_or_create(name=name)[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def handle_import_products_file(csv_file):
 | 
				
			||||||
 | 
					    data = io.StringIO(csv_file.read().decode('utf-8'))
 | 
				
			||||||
 | 
					    reader = csv.DictReader(data, quotechar='"')
 | 
				
			||||||
 | 
					    for row in reader:
 | 
				
			||||||
 | 
					        product, created = Product.objects.update_or_create(
 | 
				
			||||||
 | 
					            name=row['producto'],
 | 
				
			||||||
 | 
					            defaults={
 | 
				
			||||||
 | 
					                'price': row['precio'],
 | 
				
			||||||
 | 
					                'measuring_unit': row['unidad']
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        categories = _categories_from_csv_string(row["categorias"])
 | 
				
			||||||
 | 
					        product.categories.clear()
 | 
				
			||||||
 | 
					        for category in categories:
 | 
				
			||||||
 | 
					            product.categories.add(category)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user