diff --git a/config/settings/base.py b/config/settings/base.py index 9195225..2e594a8 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -37,7 +37,6 @@ THIRD_PARTY_APPS = [ ] PROJECT_APPS = [ - "files", "shop", "theme", "users", diff --git a/files/__init__.py b/files/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/files/admin.py b/files/admin.py deleted file mode 100644 index 8f94362..0000000 --- a/files/admin.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.contrib import admin - -from files.models import FileUpload - - -# Register your models here. -@admin.register(FileUpload) -class FileUploadAdmin(admin.ModelAdmin): - list_display = ( - "id", - "file", - ) diff --git a/files/api/__init__.py b/files/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/files/api/v1/__init__.py b/files/api/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/files/api/v1/serializers.py b/files/api/v1/serializers.py deleted file mode 100644 index 1797c69..0000000 --- a/files/api/v1/serializers.py +++ /dev/null @@ -1,16 +0,0 @@ -from rest_framework import serializers - -from files.models import FileUpload - - -class FileUploadSerializer(serializers.ModelSerializer): - class Meta: - model = FileUpload - fields = ("file", "id") - read_only_fields = ("id",) - - -class NoIDFileUploadSerializer(serializers.ModelSerializer): - class Meta: - model = FileUpload - fields = ("file",) diff --git a/files/api/v1/urls.py b/files/api/v1/urls.py deleted file mode 100644 index 88cbbd5..0000000 --- a/files/api/v1/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework.routers import DefaultRouter - -from files.api.v1.views import FileUploadViewSet - -app_name = "files" - - -router = DefaultRouter(trailing_slash=True) -router.register("", FileUploadViewSet) - -urlpatterns = router.urls diff --git a/files/api/v1/views.py b/files/api/v1/views.py deleted file mode 100644 index 56ee215..0000000 --- a/files/api/v1/views.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from files.api.v1.serializers import FileUploadSerializer -from files.models import FileUpload - - -class FileUploadViewSet(ModelViewSet): - serializer_class = FileUploadSerializer - queryset = FileUpload.objects.all() diff --git a/files/apps.py b/files/apps.py deleted file mode 100644 index a5b5712..0000000 --- a/files/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class FilesConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "files" diff --git a/files/migrations/0001_initial.py b/files/migrations/0001_initial.py deleted file mode 100644 index cac7f3d..0000000 --- a/files/migrations/0001_initial.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.0.3 on 2024-05-04 22:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="FileUpload", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("file", models.FileField(upload_to="uploads")), - ], - ), - ] diff --git a/files/migrations/__init__.py b/files/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/files/models.py b/files/models.py deleted file mode 100644 index 4d58179..0000000 --- a/files/models.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.db import models - - -class FileUpload(models.Model): - file = models.FileField(upload_to="uploads", blank=False, null=False) - - def __str__(self): - return self.file.url diff --git a/files/tests.py b/files/tests.py deleted file mode 100644 index a39b155..0000000 --- a/files/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/files/views.py b/files/views.py deleted file mode 100644 index 60f00ef..0000000 --- a/files/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. diff --git a/shop/admin.py b/shop/admin.py index 3081224..7a71523 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -12,6 +12,7 @@ from shop.models import ( ProductBatch, ProductCategory, ProductPrice, + ProductImage, Provider, ShippingMethod, ShopSettings, @@ -166,3 +167,11 @@ class ShopSettingsAdmin(ModelAdmin): "currency_code", "terminal", ) + + +@admin.register(ProductImage) +class ProductImageAdmin(ModelAdmin): + list_display = ( + "id", + "original", + ) diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py index 43f3d65..be28736 100644 --- a/shop/migrations/0001_initial.py +++ b/shop/migrations/0001_initial.py @@ -1,10 +1,10 @@ -# Generated by Django 5.1.3 on 2025-01-03 11:17 - -import uuid -from decimal import Decimal +# Generated by Django 5.1.4 on 2025-02-18 13:32 import django.db.models.deletion import django.utils.timezone +import shop.models +import uuid +from decimal import Decimal from django.conf import settings from django.db import migrations, models @@ -14,7 +14,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("files", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -115,6 +114,258 @@ class Migration(migrations.Migration): "verbose_name_plural": "proveedores", }, ), + migrations.CreateModel( + name="ShopSettings", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "merchant_code", + models.CharField( + max_length=9, verbose_name="Identificación de comercio" + ), + ), + ( + "currency_code", + models.CharField( + choices=[ + ("8", "LEK ALL"), + ("12", "ALGERIAN DINAR DZD"), + ("24", "ANGOLA KWANZA AOK"), + ("30", "PROBANDO DESA MON"), + ("31", "AZERBAIJANIAN MANAT AZM"), + ("32", "ARGENTINE AUSTRAL ARP"), + ("36", "AUSTRALIAN DOLLAR AUD"), + ("44", "BAHAMIAN DOLLAR BSD"), + ("48", "BAHRAINI DINAR BHD"), + ("50", "TAKA BDT"), + ("51", "ARMENIAN DRAM AMD"), + ("52", "BARBADOS DOLLAR BBD"), + ("60", "BERMUDAN DOLLAR BMD"), + ("64", "NGULTRUM BTN"), + ("68", "BOLIVIAN PESO BOP"), + ("70", "DINAR BAD"), + ("72", "PULA BWP"), + ("76", "CRUZEIRO BRC"), + ("84", "BELIZE DOLLAR 084"), + ("90", "SOLOMON ISLANDS DOLL SBD"), + ("96", "BRUNEI DOLLAR BND"), + ("100", "LEV BGL"), + ("104", "KYAT BUK"), + ("108", "BURUNDI FRANC BIF"), + ("112", "BELARUSSIAN RUBLE BYB"), + ("116", "RIEL KHR"), + ("124", "CANADIAN DOLLAR CAD"), + ("132", "CAPE VERDE ESCUDO CVE"), + ("136", "CAYMAN ISLANDS DOLLA KYD"), + ("144", "SRI LANKA RUPEE LKR"), + ("152", "CHILEAN PESO CLP"), + ("156", "YUAN RENMINBI CNY"), + ("157", "CHINESE RENMIMBI CNH"), + ("158", "CHINESE RENMINBI CNX"), + ("170", "COLOMBIAN PESO COP"), + ("174", "COMOROS FRANC KMF"), + ("180", "ZAIRE ZRZ"), + ("188", "COSTA RICA COLON CRC"), + ("191", "CROATIAN KUNA HRK"), + ("192", "CUBAN PESO CUP"), + ("196", "CYPRUS POUND CYP"), + ("200", "KORUNA CSK"), + ("203", "CZECH KORUNA CZK"), + ("208", "DANISH KRONE DKK"), + ("214", "DOMINICAN PESO DOP"), + ("218", "SUCRE ECS"), + ("222", "EL SALVADOR COLON SVC"), + ("226", "EKWELE GQE"), + ("230", "ETHIOPIAN BIRR ETB"), + ("232", "ERITREAN NAKTAN ERN"), + ("233", "ESTONIAN KROON EEK"), + ("238", "FALKLAND ISLANDS FKP"), + ("242", "FIJI DOLLAR FJD"), + ("262", "DJIBOUTI FRANC DJF"), + ("268", "GEORGIAN LARI GEL"), + ("270", "DALASI GMD"), + ("278", "MARK DER DDR DDM"), + ("288", "GHANA CEDI GHC"), + ("292", "GIBRALTAR POUND GIP"), + ("320", "QUETZAL GTQ"), + ("324", "SYLI GNS"), + ("328", "GUYANA DOLLAR GYD"), + ("332", "GOURDE HTG"), + ("340", "LEMPIRA HNL"), + ("344", "HONG KONG DOLLAR HKD"), + ("348", "FORINT HUF"), + ("352", "ICELAND KRONA ISK"), + ("356", "INDIAN RUPEE INR"), + ("360", "RUPIAH IDR"), + ("364", "IRANIAL RIAL IRR"), + ("365", "IRANIAN AIRLINE RATE IRA"), + ("368", "IRAQI DINAR IQD"), + ("376", "ISRAEL SHEKEL ILS"), + ("388", "JAMAICAN DOLLAR JMD"), + ("392", "YEN JPY"), + ("398", "TENGE KZT"), + ("400", "JORDANIAN DINAR JOD"), + ("404", "KENYAN SHILLING KES"), + ("408", "NORTH KOREAN WON KPW"), + ("410", "KOREAN WON KRW"), + ("414", "KUWAITI DINAR KWD"), + ("417", "KYRGYZSTAN SON KGS"), + ("418", "KIP LAK"), + ("422", "LEBANESE POUND LBP"), + ("426", "LESOTHO LOTI LSM"), + ("428", "LATVIAN LAT LVL"), + ("430", "LIBERIAN DOLLAR LRD"), + ("434", "LIBYAN DINAR LYD"), + ("440", "LITHUANIAN LITAS LTL"), + ("446", "PATACA MOP"), + ("450", "MALAGASY FRANC MGF"), + ("454", "MALAWI KWACHA MWK"), + ("458", "MALASYAN RINGGIT MYR"), + ("462", "MALDIVE RUPEE MVR"), + ("466", "MALI MLF"), + ("470", "MALTESE LIRA MTL"), + ("478", "OUGUIYA MRO"), + ("480", "MAURITIUS RUPEE MUR"), + ("484", "MEXICAN PESO MXP"), + ("496", "TUGRIK MNT"), + ("498", "MOLDOVIAN LEU MDL"), + ("504", "MORROCAN DIRHAM MAD"), + ("508", "METICAL MZM"), + ("512", "RIAL OMANI OMR"), + ("516", "NAMIBIAN DOLLAR NAD"), + ("524", "NEPALESE RUPEE NPR"), + ("532", "NETHERLANDS ANTILLIA ANG"), + ("533", "ARUBA AWG"), + ("536", "YUGOSLAVIAN NEW DIAN NTZ"), + ("548", "VANUATU VATU VUV"), + ("554", "NEW ZEALAND DOLLAR NZD"), + ("556", "NAIRA 566"), + ("558", "CORDOBA NIC"), + ("566", "NAIRA NGN"), + ("578", "NORWEGIAN KRONE NOK"), + ("582", "PACIFIC ISLAND PCI"), + ("586", "PAKISTAN RUPEE PKR"), + ("590", "BALBOA PAB"), + ("598", "KINA PGK"), + ("600", "GUARANI PYG"), + ("604", "PERU INTI PEI"), + ("608", "PHILIPPINE PESO PHP"), + ("616", "ZLOTY PLZ"), + ("624", "GUINEA"), + ("626", "TIMOR ESCUDO TPE"), + ("634", "QATARI RIAL QAR"), + ("642", "LEU ROL"), + ("643", "RUSSIAN ROUBLE RUB"), + ("646", "RWANDA FRANC RWF"), + ("654", "ST.HELENA POUND SHP"), + ("678", "DOBRA STD"), + ("682", "SAUDI RIYAL SAR"), + ("690", "SEYCHELLES RUPEE SCR"), + ("694", "LEONE SLL"), + ("702", "SINGAPORE DOLLAR SGD"), + ("703", "SLOVAK KORUNA SKK"), + ("704", "DONG VND"), + ("705", "SLOVENIAN TOLAR SIT"), + ("706", "SOMALI SHILLING SOS"), + ("710", "RAND ZAR"), + ("716", "ZIMBABWE DOLLAR ZWD"), + ("720", "YEMENI DINAR YDD"), + ("728", "SOUTH SUDANESE POUND SSP"), + ("736", "SUDANESE POUND SDP"), + ("737", "SUDAN AIRLINES SDA"), + ("740", "SURINAM GUILDER SRG"), + ("748", "LILANGENI SZL"), + ("752", "SWEDISH KRONA SEK"), + ("756", "SWISS FRANC CHF"), + ("760", "SYRIAN POUND SYP"), + ("762", "TAJIK RUBLE TJR"), + ("764", "BAHT THB"), + ("776", "PA'ANGA TOP"), + ("780", "TRINIDAD Y TOBAGO DO TTD"), + ("784", "UAE DIRHAM AED"), + ("788", "TUNISIAN DINAR TND"), + ("792", "TURKISH LIRA TRL"), + ("793", "PSEUDO TURKISH LIRA PTL"), + ("795", "MANAT TMM"), + ("800", "UGANDA SHILLING UGS"), + ("804", "KARBOVANET UAK"), + ("807", "MACEDONIAN DENAR MKD"), + ("810", "RUSSIAN ROUBLE RUR"), + ("818", "EGYPTIAN POUND EGP"), + ("826", "POUND STERLING GBP"), + ("834", "TANZANIAN SHILLING TZS"), + ("840", "DOLAR U.S.A. USD"), + ("858", "URUGUAYAN PESO UYP"), + ("860", "UZBEKISTAN SUM UZS"), + ("862", "BOLIVAR VEB"), + ("882", "TALA WST"), + ("886", "YEMINI RIAL YER"), + ("890", "NEW YUGOSLAVIAN DOLL YUD"), + ("891", "NEW DINAR YUG"), + ("894", "KWACHA ZMK"), + ("901", "NEW TAIWAN DOLLAR TWD"), + ("934", "NEW MANAT TMT"), + ("936", "GHANA CEDI GHS"), + ("941", "DINAR SERBIO RSD"), + ("943", "MOZAMBIQUE METICAL MZN"), + ("944", "AZERBAIJANIAN MANAT AZN"), + ("946", "NEW LEU RON"), + ("949", "TURKISH LIRA TRY"), + ("950", "CFA FRANC BEAC XAF"), + ("951", "EAST CARIBBEAN DOLLA XCD"), + ("952", "CFA FRANC BCEAO XOF"), + ("953", "CFP FRANC XPF"), + ("954", "E.C.U. EUROPEAN CUR XEU"), + ("967", "KWACHA ZMW"), + ("968", "SURINAME DOLLAR SRD"), + ("969", "ARIARY MGA"), + ("971", "AFGHANISTAN AFGHANI AFN"), + ("972", "TAJIKISTAN SOMONI TJS"), + ("973", "KWANZA ANGOLA AOA"), + ("974", "BELARUSSIAN RUBLE BYR"), + ("975", "NEW LEV BGN"), + ("976", "FRANCO DEL CONGO CDF"), + ("977", "BOSNIAN MARKA BAM"), + ("978", "EURO EUR"), + ("980", "HRYVNIA UAH"), + ("981", "GEORGIAN LARI GEL"), + ("985", "NEW POLISH ZLOTY PLN"), + ("986", "BRAZILIAN REAL BRL"), + ("991", "RAND FINANCIER ZAL"), + ], + default="978", + max_length=4, + verbose_name="Código de moneda", + ), + ), + ("terminal", models.CharField(max_length=8, verbose_name="Terminal")), + ( + "shared_secret", + models.CharField(max_length=100, verbose_name="Clave de Redsys"), + ), + ( + "tpv_domain", + models.URLField( + blank=True, + max_length=128, + null=True, + verbose_name="dominio del tpv", + ), + ), + ], + options={ + "verbose_name": "ajustes de la tienda", + "verbose_name_plural": "ajustes de la tienda", + }, + ), migrations.CreateModel( name="Tag", fields=[ @@ -332,12 +583,26 @@ class Migration(migrations.Migration): db_index=True, default=uuid.uuid4, verbose_name="UUID" ), ), + ( + "code", + models.CharField( + default=shop.models.create_order_code, + max_length=20, + unique=True, + ), + ), ( "status", models.CharField( default="PEN", max_length=3, verbose_name="estado" ), ), + ( + "shipping_status", + models.CharField( + default="NOT", max_length=3, verbose_name="estado de envío" + ), + ), ( "base_total", models.DecimalField( @@ -416,6 +681,25 @@ class Migration(migrations.Migration): blank=True, max_length=32, verbose_name="teléfono de contacto" ), ), + ( + "amount_paid", + models.DecimalField( + decimal_places=2, + default=Decimal("0.00"), + max_digits=11, + verbose_name="cantidad pagada", + ), + ), + ( + "from_cart", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="shop.cart", + verbose_name="carrito de origen de pedido", + ), + ), ( "user", models.ForeignKey( @@ -430,6 +714,69 @@ class Migration(migrations.Migration): options={ "verbose_name": "pedido", "verbose_name_plural": "pedidos", + "ordering": ("creation_date",), + }, + ), + migrations.CreateModel( + name="Payment", + fields=[ + ( + "hash", + models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + verbose_name="Hash", + ), + ), + ( + "amount", + models.DecimalField( + decimal_places=2, + default=Decimal("0"), + max_digits=8, + verbose_name="Cantidad", + ), + ), + ( + "creation_date", + models.DateTimeField( + auto_now_add=True, verbose_name="Fecha de creación" + ), + ), + ("metadata", models.JSONField(default=dict, verbose_name="Metadata")), + ( + "method", + models.CharField( + choices=[("REDSYS", "Redsys")], + default="REDSYS", + max_length=6, + verbose_name="método de pago", + ), + ), + ( + "order", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="payments", + to="shop.order", + verbose_name="pedido", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + verbose_name="cliente", + ), + ), + ], + options={ + "verbose_name": "Pago", + "verbose_name_plural": "Pagos", }, ), migrations.CreateModel( @@ -501,6 +848,12 @@ class Migration(migrations.Migration): db_index=True, default=False, verbose_name="oculto" ), ), + ( + "is_shipping_method", + models.BooleanField( + default=False, verbose_name="es forma de envío" + ), + ), ( "brand", models.ForeignKey( @@ -511,12 +864,6 @@ class Migration(migrations.Migration): verbose_name="marca", ), ), - ( - "images", - models.ManyToManyField( - blank=True, to="files.fileupload", verbose_name="imágenes" - ), - ), ], options={ "verbose_name": "producto", @@ -708,6 +1055,110 @@ class Migration(migrations.Migration): blank=True, to="shop.productcategory", verbose_name="categorías" ), ), + migrations.CreateModel( + name="ProductImage", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "original", + models.ImageField( + upload_to="uploads", verbose_name="Imagen original" + ), + ), + ( + "xl", + models.ImageField( + blank=True, null=True, upload_to="uploads", verbose_name="XL" + ), + ), + ( + "xl_dark", + models.ImageField( + blank=True, + null=True, + upload_to="uploads", + verbose_name="XL (fondo oscuro)", + ), + ), + ( + "l", + models.ImageField( + blank=True, null=True, upload_to="uploads", verbose_name="L" + ), + ), + ( + "l_dark", + models.ImageField( + blank=True, + null=True, + upload_to="uploads", + verbose_name="L (fondo oscuro)", + ), + ), + ( + "m", + models.ImageField( + blank=True, null=True, upload_to="uploads", verbose_name="M" + ), + ), + ( + "m_dark", + models.ImageField( + blank=True, + null=True, + upload_to="uploads", + verbose_name="M (fondo oscuro)", + ), + ), + ( + "s", + models.ImageField( + blank=True, null=True, upload_to="uploads", verbose_name="S" + ), + ), + ( + "s_dark", + models.ImageField( + blank=True, + null=True, + upload_to="uploads", + verbose_name="S (fondo oscuro)", + ), + ), + ( + "xs", + models.ImageField( + blank=True, null=True, upload_to="uploads", verbose_name="XS" + ), + ), + ( + "xs_dark", + models.ImageField( + blank=True, + null=True, + upload_to="uploads", + verbose_name="XS (fondo oscuro)", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="images", + to="shop.product", + verbose_name="Producto", + ), + ), + ], + ), migrations.CreateModel( name="ProductBatch", fields=[ @@ -820,6 +1271,17 @@ class Migration(migrations.Migration): "verbose_name_plural": "métodos de envío", }, ), + migrations.AddField( + model_name="order", + name="shipping_method", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="shop.shippingmethod", + verbose_name="método de envío", + ), + ), migrations.AddField( model_name="product", name="tags", diff --git a/shop/migrations/0002_shopsettings_order_code_order_shipping_status_and_more.py b/shop/migrations/0002_shopsettings_order_code_order_shipping_status_and_more.py deleted file mode 100644 index d171eab..0000000 --- a/shop/migrations/0002_shopsettings_order_code_order_shipping_status_and_more.py +++ /dev/null @@ -1,344 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-12 13:03 - -import uuid -from decimal import Decimal - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import shop.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("shop", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="ShopSettings", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "merchant_code", - models.CharField( - max_length=9, verbose_name="Identificación de comercio" - ), - ), - ( - "currency_code", - models.CharField( - choices=[ - ("8", "LEK ALL"), - ("12", "ALGERIAN DINAR DZD"), - ("24", "ANGOLA KWANZA AOK"), - ("30", "PROBANDO DESA MON"), - ("31", "AZERBAIJANIAN MANAT AZM"), - ("32", "ARGENTINE AUSTRAL ARP"), - ("36", "AUSTRALIAN DOLLAR AUD"), - ("44", "BAHAMIAN DOLLAR BSD"), - ("48", "BAHRAINI DINAR BHD"), - ("50", "TAKA BDT"), - ("51", "ARMENIAN DRAM AMD"), - ("52", "BARBADOS DOLLAR BBD"), - ("60", "BERMUDAN DOLLAR BMD"), - ("64", "NGULTRUM BTN"), - ("68", "BOLIVIAN PESO BOP"), - ("70", "DINAR BAD"), - ("72", "PULA BWP"), - ("76", "CRUZEIRO BRC"), - ("84", "BELIZE DOLLAR 084"), - ("90", "SOLOMON ISLANDS DOLL SBD"), - ("96", "BRUNEI DOLLAR BND"), - ("100", "LEV BGL"), - ("104", "KYAT BUK"), - ("108", "BURUNDI FRANC BIF"), - ("112", "BELARUSSIAN RUBLE BYB"), - ("116", "RIEL KHR"), - ("124", "CANADIAN DOLLAR CAD"), - ("132", "CAPE VERDE ESCUDO CVE"), - ("136", "CAYMAN ISLANDS DOLLA KYD"), - ("144", "SRI LANKA RUPEE LKR"), - ("152", "CHILEAN PESO CLP"), - ("156", "YUAN RENMINBI CNY"), - ("157", "CHINESE RENMIMBI CNH"), - ("158", "CHINESE RENMINBI CNX"), - ("170", "COLOMBIAN PESO COP"), - ("174", "COMOROS FRANC KMF"), - ("180", "ZAIRE ZRZ"), - ("188", "COSTA RICA COLON CRC"), - ("191", "CROATIAN KUNA HRK"), - ("192", "CUBAN PESO CUP"), - ("196", "CYPRUS POUND CYP"), - ("200", "KORUNA CSK"), - ("203", "CZECH KORUNA CZK"), - ("208", "DANISH KRONE DKK"), - ("214", "DOMINICAN PESO DOP"), - ("218", "SUCRE ECS"), - ("222", "EL SALVADOR COLON SVC"), - ("226", "EKWELE GQE"), - ("230", "ETHIOPIAN BIRR ETB"), - ("232", "ERITREAN NAKTAN ERN"), - ("233", "ESTONIAN KROON EEK"), - ("238", "FALKLAND ISLANDS FKP"), - ("242", "FIJI DOLLAR FJD"), - ("262", "DJIBOUTI FRANC DJF"), - ("268", "GEORGIAN LARI GEL"), - ("270", "DALASI GMD"), - ("278", "MARK DER DDR DDM"), - ("288", "GHANA CEDI GHC"), - ("292", "GIBRALTAR POUND GIP"), - ("320", "QUETZAL GTQ"), - ("324", "SYLI GNS"), - ("328", "GUYANA DOLLAR GYD"), - ("332", "GOURDE HTG"), - ("340", "LEMPIRA HNL"), - ("344", "HONG KONG DOLLAR HKD"), - ("348", "FORINT HUF"), - ("352", "ICELAND KRONA ISK"), - ("356", "INDIAN RUPEE INR"), - ("360", "RUPIAH IDR"), - ("364", "IRANIAL RIAL IRR"), - ("365", "IRANIAN AIRLINE RATE IRA"), - ("368", "IRAQI DINAR IQD"), - ("376", "ISRAEL SHEKEL ILS"), - ("388", "JAMAICAN DOLLAR JMD"), - ("392", "YEN JPY"), - ("398", "TENGE KZT"), - ("400", "JORDANIAN DINAR JOD"), - ("404", "KENYAN SHILLING KES"), - ("408", "NORTH KOREAN WON KPW"), - ("410", "KOREAN WON KRW"), - ("414", "KUWAITI DINAR KWD"), - ("417", "KYRGYZSTAN SON KGS"), - ("418", "KIP LAK"), - ("422", "LEBANESE POUND LBP"), - ("426", "LESOTHO LOTI LSM"), - ("428", "LATVIAN LAT LVL"), - ("430", "LIBERIAN DOLLAR LRD"), - ("434", "LIBYAN DINAR LYD"), - ("440", "LITHUANIAN LITAS LTL"), - ("446", "PATACA MOP"), - ("450", "MALAGASY FRANC MGF"), - ("454", "MALAWI KWACHA MWK"), - ("458", "MALASYAN RINGGIT MYR"), - ("462", "MALDIVE RUPEE MVR"), - ("466", "MALI MLF"), - ("470", "MALTESE LIRA MTL"), - ("478", "OUGUIYA MRO"), - ("480", "MAURITIUS RUPEE MUR"), - ("484", "MEXICAN PESO MXP"), - ("496", "TUGRIK MNT"), - ("498", "MOLDOVIAN LEU MDL"), - ("504", "MORROCAN DIRHAM MAD"), - ("508", "METICAL MZM"), - ("512", "RIAL OMANI OMR"), - ("516", "NAMIBIAN DOLLAR NAD"), - ("524", "NEPALESE RUPEE NPR"), - ("532", "NETHERLANDS ANTILLIA ANG"), - ("533", "ARUBA AWG"), - ("536", "YUGOSLAVIAN NEW DIAN NTZ"), - ("548", "VANUATU VATU VUV"), - ("554", "NEW ZEALAND DOLLAR NZD"), - ("556", "NAIRA 566"), - ("558", "CORDOBA NIC"), - ("566", "NAIRA NGN"), - ("578", "NORWEGIAN KRONE NOK"), - ("582", "PACIFIC ISLAND PCI"), - ("586", "PAKISTAN RUPEE PKR"), - ("590", "BALBOA PAB"), - ("598", "KINA PGK"), - ("600", "GUARANI PYG"), - ("604", "PERU INTI PEI"), - ("608", "PHILIPPINE PESO PHP"), - ("616", "ZLOTY PLZ"), - ("624", "GUINEA"), - ("626", "TIMOR ESCUDO TPE"), - ("634", "QATARI RIAL QAR"), - ("642", "LEU ROL"), - ("643", "RUSSIAN ROUBLE RUB"), - ("646", "RWANDA FRANC RWF"), - ("654", "ST.HELENA POUND SHP"), - ("678", "DOBRA STD"), - ("682", "SAUDI RIYAL SAR"), - ("690", "SEYCHELLES RUPEE SCR"), - ("694", "LEONE SLL"), - ("702", "SINGAPORE DOLLAR SGD"), - ("703", "SLOVAK KORUNA SKK"), - ("704", "DONG VND"), - ("705", "SLOVENIAN TOLAR SIT"), - ("706", "SOMALI SHILLING SOS"), - ("710", "RAND ZAR"), - ("716", "ZIMBABWE DOLLAR ZWD"), - ("720", "YEMENI DINAR YDD"), - ("728", "SOUTH SUDANESE POUND SSP"), - ("736", "SUDANESE POUND SDP"), - ("737", "SUDAN AIRLINES SDA"), - ("740", "SURINAM GUILDER SRG"), - ("748", "LILANGENI SZL"), - ("752", "SWEDISH KRONA SEK"), - ("756", "SWISS FRANC CHF"), - ("760", "SYRIAN POUND SYP"), - ("762", "TAJIK RUBLE TJR"), - ("764", "BAHT THB"), - ("776", "PA'ANGA TOP"), - ("780", "TRINIDAD Y TOBAGO DO TTD"), - ("784", "UAE DIRHAM AED"), - ("788", "TUNISIAN DINAR TND"), - ("792", "TURKISH LIRA TRL"), - ("793", "PSEUDO TURKISH LIRA PTL"), - ("795", "MANAT TMM"), - ("800", "UGANDA SHILLING UGS"), - ("804", "KARBOVANET UAK"), - ("807", "MACEDONIAN DENAR MKD"), - ("810", "RUSSIAN ROUBLE RUR"), - ("818", "EGYPTIAN POUND EGP"), - ("826", "POUND STERLING GBP"), - ("834", "TANZANIAN SHILLING TZS"), - ("840", "DOLAR U.S.A. USD"), - ("858", "URUGUAYAN PESO UYP"), - ("860", "UZBEKISTAN SUM UZS"), - ("862", "BOLIVAR VEB"), - ("882", "TALA WST"), - ("886", "YEMINI RIAL YER"), - ("890", "NEW YUGOSLAVIAN DOLL YUD"), - ("891", "NEW DINAR YUG"), - ("894", "KWACHA ZMK"), - ("901", "NEW TAIWAN DOLLAR TWD"), - ("934", "NEW MANAT TMT"), - ("936", "GHANA CEDI GHS"), - ("941", "DINAR SERBIO RSD"), - ("943", "MOZAMBIQUE METICAL MZN"), - ("944", "AZERBAIJANIAN MANAT AZN"), - ("946", "NEW LEU RON"), - ("949", "TURKISH LIRA TRY"), - ("950", "CFA FRANC BEAC XAF"), - ("951", "EAST CARIBBEAN DOLLA XCD"), - ("952", "CFA FRANC BCEAO XOF"), - ("953", "CFP FRANC XPF"), - ("954", "E.C.U. EUROPEAN CUR XEU"), - ("967", "KWACHA ZMW"), - ("968", "SURINAME DOLLAR SRD"), - ("969", "ARIARY MGA"), - ("971", "AFGHANISTAN AFGHANI AFN"), - ("972", "TAJIKISTAN SOMONI TJS"), - ("973", "KWANZA ANGOLA AOA"), - ("974", "BELARUSSIAN RUBLE BYR"), - ("975", "NEW LEV BGN"), - ("976", "FRANCO DEL CONGO CDF"), - ("977", "BOSNIAN MARKA BAM"), - ("978", "EURO EUR"), - ("980", "HRYVNIA UAH"), - ("981", "GEORGIAN LARI GEL"), - ("985", "NEW POLISH ZLOTY PLN"), - ("986", "BRAZILIAN REAL BRL"), - ("991", "RAND FINANCIER ZAL"), - ], - default="978", - max_length=4, - verbose_name="Código de moneda", - ), - ), - ("terminal", models.CharField(max_length=8, verbose_name="Terminal")), - ( - "shared_secret", - models.CharField(max_length=100, verbose_name="Clave de Redsys"), - ), - ], - options={ - "verbose_name": "ajustes de la tienda", - "verbose_name_plural": "ajustes de la tienda", - }, - ), - migrations.AddField( - model_name="order", - name="code", - field=models.CharField( - default=shop.models.create_order_code, max_length=20, unique=True - ), - ), - migrations.AddField( - model_name="order", - name="shipping_status", - field=models.CharField( - default="NOT", max_length=3, verbose_name="estado de envío" - ), - ), - migrations.AddField( - model_name="product", - name="is_shipping_method", - field=models.BooleanField(default=False, verbose_name="es forma de envío"), - ), - migrations.CreateModel( - name="Payment", - fields=[ - ( - "hash", - models.UUIDField( - default=uuid.uuid4, - primary_key=True, - serialize=False, - verbose_name="Hash", - ), - ), - ( - "amount", - models.DecimalField( - decimal_places=2, - default=Decimal("0"), - max_digits=8, - verbose_name="Cantidad", - ), - ), - ( - "creation_date", - models.DateTimeField( - auto_now_add=True, verbose_name="Fecha de creación" - ), - ), - ("metadata", models.JSONField(default=dict, verbose_name="Metadata")), - ( - "method", - models.CharField( - choices=[("REDSYS", "Redsys")], - default="REDSYS", - max_length=6, - verbose_name="método de pago", - ), - ), - ( - "order", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="shop.order", - verbose_name="pedido", - ), - ), - ( - "user", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - to=settings.AUTH_USER_MODEL, - verbose_name="cliente", - ), - ), - ], - options={ - "verbose_name": "Pago", - "verbose_name_plural": "Pagos", - }, - ), - ] diff --git a/shop/migrations/0003_order_shipping_method_alter_payment_order.py b/shop/migrations/0003_order_shipping_method_alter_payment_order.py deleted file mode 100644 index e9f9a9a..0000000 --- a/shop/migrations/0003_order_shipping_method_alter_payment_order.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-12 22:15 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("shop", "0002_shopsettings_order_code_order_shipping_status_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="order", - name="shipping_method", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="shop.shippingmethod", - verbose_name="método de envío", - ), - ), - migrations.AlterField( - model_name="payment", - name="order", - field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="payments", - to="shop.order", - verbose_name="pedido", - ), - ), - ] diff --git a/shop/migrations/0004_shopsettings_tpv_domain.py b/shop/migrations/0004_shopsettings_tpv_domain.py deleted file mode 100644 index cc196ab..0000000 --- a/shop/migrations/0004_shopsettings_tpv_domain.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-13 08:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("shop", "0003_order_shipping_method_alter_payment_order"), - ] - - operations = [ - migrations.AddField( - model_name="shopsettings", - name="tpv_domain", - field=models.URLField( - blank=True, max_length=128, null=True, verbose_name="dominio del tpv" - ), - ), - ] diff --git a/shop/migrations/0005_alter_order_options_order_amount_paid_and_more.py b/shop/migrations/0005_alter_order_options_order_amount_paid_and_more.py deleted file mode 100644 index 3061fd5..0000000 --- a/shop/migrations/0005_alter_order_options_order_amount_paid_and_more.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-16 17:00 - -from decimal import Decimal - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("shop", "0004_shopsettings_tpv_domain"), - ] - - operations = [ - migrations.AlterModelOptions( - name="order", - options={ - "ordering": ("creation_date",), - "verbose_name": "pedido", - "verbose_name_plural": "pedidos", - }, - ), - migrations.AddField( - model_name="order", - name="amount_paid", - field=models.DecimalField( - decimal_places=2, - default=Decimal("0.00"), - max_digits=11, - verbose_name="cantidad pagada", - ), - ), - migrations.AddField( - model_name="order", - name="from_cart", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="shop.cart", - verbose_name="carrito de origen de pedido", - ), - ), - ] diff --git a/shop/models.py b/shop/models.py index c673be9..dbe56f8 100644 --- a/shop/models.py +++ b/shop/models.py @@ -1,13 +1,17 @@ from decimal import Decimal +from io import BytesIO from uuid import uuid4 from django.conf import settings from django.contrib.auth import get_user_model +from django.core.files.base import ContentFile from django.db import models from django.utils import timezone from django.utils.text import gettext_lazy as _ from django.utils.text import slugify +from PIL import Image + from config.models import SingletonModel from shop.settings import CURRENCY_CODES @@ -103,9 +107,6 @@ class Product(TimestampedModel): default=False, verbose_name=_("es un activo digital") ) url = models.URLField(blank=True, verbose_name=_("URL de descarga")) - images = models.ManyToManyField( - "files.FileUpload", blank=True, verbose_name=_("imágenes") - ) tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("etiquetas")) categories = models.ManyToManyField( "shop.ProductCategory", blank=True, verbose_name=_("categorías") @@ -182,6 +183,106 @@ class ProductPrice(TimestampedModel): ordering = ("-date",) +class ProductImage(models.Model): + product = models.ForeignKey( + "shop.Product", + on_delete=models.CASCADE, + blank=False, + null=False, + verbose_name=_("Producto"), + related_name="images", + ) + original = models.ImageField( + upload_to="uploads", blank=False, null=False, verbose_name=_("Imagen original") + ) + + xl = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("XL") + ) + xl_dark = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("XL (fondo oscuro)") + ) + + l = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("L") + ) + l_dark = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("L (fondo oscuro)") + ) + + m = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("M") + ) + m_dark = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("M (fondo oscuro)") + ) + + s = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("S") + ) + s_dark = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("S (fondo oscuro)") + ) + + xs = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("XS") + ) + xs_dark = models.ImageField( + upload_to="uploads", blank=True, null=True, verbose_name=_("XS (fondo oscuro)") + ) + + def __str__(self): + return self.original.url + + def make_square(self, image, max_size=512, fill_color="#000000"): + width, height = image.size + + if width > height: + ratio = width / max_size + else: + ratio = height / max_size + + new_width = int(width / ratio) + new_height = int(height / ratio) + + resized_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) + + new_image = Image.new("RGB", (max_size, max_size), fill_color) + + position_x = int((max_size - new_width) / 2) + position_y = int((max_size - new_height) / 2) + + new_image.paste(resized_image, (position_x, position_y)) + + return new_image + + def get_resized_image(self, image, size, fill_color="#000000"): + image = Image.open(image) + image = self.make_square(image, size, fill_color) + buffer = BytesIO() + image.save(fp=buffer, format="WEBP") + + return ContentFile(buffer.getvalue(), name=f"{self.product.slug}_{size}.webp") + + def save(self, *args, **kwargs): + self.xl = self.get_resized_image(self.original, 1024, fill_color="#FFFFFF") + self.xl_dark = self.get_resized_image(self.original, 1024) + + self.l = self.get_resized_image(self.original, 512, fill_color="#FFFFFF") + self.l_dark = self.get_resized_image(self.original, 512) + + self.m = self.get_resized_image(self.original, 256, fill_color="#FFFFFF") + self.m_dark = self.get_resized_image(self.original, 256) + + self.s = self.get_resized_image(self.original, 128, fill_color="#FFFFFF") + self.s_dark = self.get_resized_image(self.original, 128) + + self.xs = self.get_resized_image(self.original, 96, fill_color="#FFFFFF") + self.xs_dark = self.get_resized_image(self.original, 96) + + return super().save(*args, **kwargs) + + class ProductBatch(TimestampedModel): code = models.CharField( max_length=32, unique=True, db_index=True, verbose_name=_("código") diff --git a/shop/tests/test_images.py b/shop/tests/test_images.py new file mode 100644 index 0000000..29a2699 --- /dev/null +++ b/shop/tests/test_images.py @@ -0,0 +1,50 @@ +from io import BytesIO + +from PIL import Image +from django.core.files.base import ContentFile +from django.test import TestCase + +from shop.models import ProductImage +from shop.tests.mixins import CreateProductsMixin + + +class TestProductImages(TestCase, CreateProductsMixin): + def setUp(self): + self.product = self.create_product() + + def create_image_file(self, size=(768, 768)): + image = Image.new("RGB", size, "#ACACAC") + + buffer = BytesIO() + image.save(fp=buffer, format="WEBP") + file = ContentFile(buffer.getvalue(), name=f"{self.product.slug}.webp") + return file + + def test_product_image_creation(self): + file = self.create_image_file() + image = ProductImage.objects.create(product=self.product, original=file) + + xl = Image.open(image.xl) + xl_dark = Image.open(image.xl_dark) + + assert (1024, 1024) == xl.size == xl_dark.size + + l = Image.open(image.l) + l_dark = Image.open(image.l_dark) + + assert (512, 512) == l.size == l_dark.size + + m = Image.open(image.m) + m_dark = Image.open(image.m_dark) + + assert (256, 256) == m.size == m_dark.size + + s = Image.open(image.s) + s_dark = Image.open(image.s_dark) + + assert (128, 128) == s.size == s_dark.size + + xs = Image.open(image.xs) + xs_dark = Image.open(image.xs_dark) + + assert (96, 96) == xs.size == xs_dark.size diff --git a/users/forms/login.py b/users/forms/login.py index 27a20ac..29424ec 100644 --- a/users/forms/login.py +++ b/users/forms/login.py @@ -30,9 +30,5 @@ class LoginForm(StylingMixin, AuthenticationForm): remember_me = forms.BooleanField( label=_("Mantener sesión iniciada"), required=False, - widget=forms.CheckboxInput( - attrs={ - "class": "input-checkbox" - } - ), + widget=forms.CheckboxInput(attrs={"class": "input-checkbox"}), ) diff --git a/web/templates/components/cart/cart.html b/web/templates/components/cart/cart.html index 86ffd4a..5e302e0 100644 --- a/web/templates/components/cart/cart.html +++ b/web/templates/components/cart/cart.html @@ -15,9 +15,9 @@