Files
proyecto-final/tienda/models.py
T

350 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from django.db import models
from django.contrib.auth.models import User, AbstractUser
from django.utils.crypto import get_random_string
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX, TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET
import random, string
def generate_transaction_code() -> str:
while True:
code = f"{TRANSACTION_CODE_PREFIX}{get_random_string(TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET)}"
if not Order.objects.filter(transaction_code=code).exists():
return code
class User(AbstractUser):
class RegisterStatus(models.TextChoices):
CONFIRMATION_REQUIRED = "CR", "Confirmation Required"
ACTIVE = "AC", "Active"
BANNED = "BN", "Banned"
registration_status = models.CharField(
max_length = 2,
choices = RegisterStatus.choices,
default = RegisterStatus.CONFIRMATION_REQUIRED
)
def to_dict(self):
return {
"username": self.username,
"fullname": self.get_full_name()
}
class VerificationCode(models.Model):
class VerificationModes(models.TextChoices):
VERIFY_ACCOUNT = "VA"
RESET_PASSWORD = "RP"
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_belongsto", null=False, blank=False)
code = models.CharField(max_length=64, default="", unique=True)
code_mode = models.CharField(
max_length=2,
choices = VerificationModes.choices,
default = VerificationModes.VERIFY_ACCOUNT
)
def generate(user: User, code_mode: str) -> VerificationCode:
while True:
code = "".join(random.choices(string.ascii_letters+string.digits, k=64))
if not VerificationCode.objects.filter(code=code).exists():
return VerificationCode.objects.create(
code = code,
user = user,
code_mode = code_mode
)
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.name
def to_dict(self):
return {
"name": self.name
}
class Image(models.Model):
name = models.CharField(max_length=200, default="")
image = models.ImageField(upload_to='images/')
alt = models.CharField(max_length=255, default="", blank=True, verbose_name="Texto alternativo")
def __str__(self):
return self.name
def to_dict(self):
return {
"name": self.name,
"image": self.image.url,
"alt": self.alt
}
class Product(models.Model):
name = models.CharField(max_length=200, default="")
description = models.TextField(default = "")
briefdesc = models.TextField(default = "")
price = models.FloatField(default = 0)
stock = models.PositiveIntegerField(default=0)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
primary_image = models.ForeignKey(Image, on_delete=models.SET_NULL, null=True)
secondary_images = models.ManyToManyField(Image, related_name='products_secondary', blank=True)
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_products', null=True, blank=True)
def __str__(self):
return self.name + " " + str(self.price)
def get_price_with_vat(self):
"""Retorna el precio con IVA incluido"""
return round(self.price * (1 + VAT_RATE), 2)
def get_vat_amount(self):
"""Retorna la cantidad de IVA"""
return round(self.price * VAT_RATE, 2)
def to_dict(self):
return {
"name": self.name,
"description": self.description,
"briefdesc": self.briefdesc,
"price": self.price,
"stock": self.stock,
"category": self.category.to_dict(),
"primary_image": self.primary_image.to_dict(),
"secondary_images": [secondary_image.to_dict() for secondary_image in self.secondary_images.all()],
"creator": self.creator.to_dict()
}
class StockReservation(models.Model):
STATUS_ACTIVE = "active"
STATUS_COMPLETED = "completed"
STATUS_CANCELLED = "cancelled"
STATUS_EXPIRED = "expired"
STATUS_CHOICES = [
(STATUS_ACTIVE, "Activa"),
(STATUS_COMPLETED, "Completada"),
(STATUS_CANCELLED, "Cancelada"),
(STATUS_EXPIRED, "Expirada"),
]
PAYMENT_STRIPE = "stripe"
PAYMENT_PAYPAL = "paypal"
PAYMENT_CHOICES = [
(PAYMENT_STRIPE, "Stripe"),
(PAYMENT_PAYPAL, "PayPal"),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name="stock_reservations")
session_key = models.CharField(max_length=40, null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_ACTIVE)
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES)
expires_at = models.DateTimeField(db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Reserva {self.id} - {self.user or self.session_key} ({self.status})"
class StockReservationItem(models.Model):
reservation = models.ForeignKey(StockReservation, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="stock_reservation_items")
quantity = models.PositiveIntegerField(default=1)
class Meta:
unique_together = ("reservation", "product")
def __str__(self):
return f"{self.quantity}x {self.product.name} (reserva {self.reservation_id})"
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
session_key = models.CharField(max_length=40, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Cart {self.id} - {self.user or self.session_key}"
def get_total(self):
return sum(item.get_subtotal() for item in self.items.all())
def get_total_with_vat(self):
"""Retorna el total del carrito con IVA incluido"""
return round(self.get_total() * (1 + VAT_RATE), 2)
def get_vat_amount(self):
"""Retorna la cantidad total de IVA"""
return round(self.get_total() * VAT_RATE, 2)
def get_items_count(self):
return sum(item.quantity for item in self.items.all())
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('cart', 'product')
def __str__(self):
return f"{self.quantity}x {self.product.name}"
def get_subtotal(self):
return self.product.price * self.quantity
def get_subtotal_with_vat(self):
"""Retorna el subtotal del item con IVA incluido"""
return round(self.get_subtotal() * (1 + VAT_RATE), 2)
def get_vat_amount(self):
"""Retorna la cantidad de IVA de este item"""
return round(self.get_subtotal() * VAT_RATE, 2)
class Order(models.Model):
STATUS_PAID = "paid"
STATUS_CANCELLED = "cancelled"
STATUS_CHOICES = [
(STATUS_PAID, "Pagado"),
(STATUS_CANCELLED, "Cancelado"),
]
PAYMENT_STRIPE = "stripe"
PAYMENT_PAYPAL = "paypal"
PAYMENT_MANUAL = "manual"
PAYMENT_CHOICES = [
(PAYMENT_STRIPE, "Stripe"),
(PAYMENT_PAYPAL, "PayPal"),
(PAYMENT_MANUAL, "Manual"),
]
buyer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
shipping_address = models.ForeignKey('ShippingAddress', on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
session_key = models.CharField(max_length=40, null=True, blank=True)
total = models.FloatField(default=0)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID)
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES, default=PAYMENT_MANUAL)
payment_reference = models.CharField(max_length=200, blank=True, default="")
transaction_code = models.CharField(max_length=38, unique=True, null=True, blank=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Pedido {self.id} - {self.buyer or self.session_key}"
def save(self, *args, **kwargs):
if not self.transaction_code:
self.transaction_code = generate_transaction_code()
super().save(*args, **kwargs)
def get_items_count(self):
return sum(item.quantity for item in self.items.all())
class OrderItem(models.Model):
STATUS_PENDING = "pending"
STATUS_PROCESSING = "processing"
STATUS_SHIPPED = "shipped"
STATUS_CHOICES = [
(STATUS_PENDING, "Pendiente"),
(STATUS_PROCESSING, "En preparación"),
(STATUS_SHIPPED, "Enviado"),
]
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, blank=True)
product_name = models.CharField(max_length=200, default="")
seller = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='order_items_to_fulfill')
quantity = models.PositiveIntegerField(default=1)
unit_price = models.FloatField(default=0)
total_price = models.FloatField(default=0)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.quantity}x {self.product_name} (Pedido {self.order_id})"
class OrderMessage(models.Model):
order_item = models.ForeignKey(OrderItem, on_delete=models.CASCADE, related_name='messages')
sender = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='sent_messages')
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f"Mensaje de {self.sender} - {self.created_at}"
class SavedPaymentMethod(models.Model):
"""Métodos de pago guardados por el usuario (tarjetas Stripe o cuentas PayPal)."""
TYPE_CARD = "card"
TYPE_PAYPAL = "paypal"
TYPE_CHOICES = [
(TYPE_CARD, "Tarjeta"),
(TYPE_PAYPAL, "PayPal"),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payment_methods")
method_type = models.CharField(max_length=10, choices=TYPE_CHOICES)
label = models.CharField(max_length=200, verbose_name="Etiqueta")
# Stripe fields
stripe_customer_id = models.CharField(max_length=100, blank=True, default="")
stripe_payment_method_id = models.CharField(max_length=100, blank=True, default="")
# PayPal fields
paypal_email = models.CharField(max_length=254, blank=True, default="")
paypal_payer_id = models.CharField(max_length=100, blank=True, default="")
is_default = models.BooleanField(default=False, verbose_name="Predeterminado")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Método de pago guardado"
verbose_name_plural = "Métodos de pago guardados"
ordering = ["-is_default", "-created_at"]
def __str__(self):
return f"{self.user.username} {self.label}"
def save(self, *args, **kwargs):
if self.is_default:
SavedPaymentMethod.objects.filter(user=self.user, is_default=True).update(is_default=False)
super().save(*args, **kwargs)
class ShippingAddress(models.Model):
"""Direcciones de entrega de los usuarios"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipping_addresses')
full_name = models.CharField(max_length=200, verbose_name="Nombre completo")
address_line_1 = models.CharField(max_length=250, verbose_name="Dirección")
address_line_2 = models.CharField(max_length=250, blank=True, verbose_name="Dirección (línea 2)")
city = models.CharField(max_length=100, verbose_name="Ciudad")
postal_code = models.CharField(max_length=20, verbose_name="Código postal")
country = models.CharField(max_length=100, default="España", verbose_name="País")
phone = models.CharField(max_length=20, verbose_name="Teléfono")
is_default = models.BooleanField(default=False, verbose_name="Dirección predeterminada")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Dirección de envío"
verbose_name_plural = "Direcciones de envío"
ordering = ['-is_default', '-created_at']
def __str__(self):
return f"{self.full_name} - {self.city}"
def save(self, *args, **kwargs):
# Si se marca como predeterminada, desmarcar las demás del usuario
if self.is_default:
ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False)
super().save(*args, **kwargs)