495 lines
21 KiB
Python
495 lines
21 KiB
Python
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
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class TimestampedModel(models.Model):
|
|
creation_date = models.DateTimeField(auto_now_add=True, verbose_name=_('fecha de creación'))
|
|
last_modification_date = models.DateTimeField(auto_now=True, verbose_name=_('fecha de última modificación'))
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
|
|
class Tag(models.Model):
|
|
name = models.CharField(max_length=16)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _('etiqueta')
|
|
verbose_name_plural = _('etiquetas')
|
|
ordering = ('name',)
|
|
|
|
|
|
class ProductCategory(models.Model):
|
|
name = models.CharField(max_length=32)
|
|
parent = models.ForeignKey(
|
|
'shop.ProductCategory', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('categoría padre')
|
|
)
|
|
promoted = models.BooleanField(default=False, verbose_name=_('promocionada'))
|
|
hidden = models.BooleanField(default=False, verbose_name=_('oculta'))
|
|
show_in_navbar = models.BooleanField(default=False, verbose_name=_('mostrar en cabecera'))
|
|
slug = models.SlugField(max_length=48, blank=True, default='', verbose_name=_('slug'), unique=True, db_index=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.slug = slugify(self.name)
|
|
super().save()
|
|
|
|
class Meta:
|
|
verbose_name = _('categoría')
|
|
verbose_name_plural = _('categorías')
|
|
ordering = ('name',)
|
|
|
|
|
|
class Product(TimestampedModel):
|
|
sku = models.CharField(max_length=64, blank=False, null=False, unique=True, verbose_name=_('código de referencia'))
|
|
name = models.CharField(max_length=96, blank=False, null=False, verbose_name=_('nombre'))
|
|
description = models.TextField(blank=True, default='', verbose_name=_('descripción'))
|
|
stock = models.DecimalField(max_digits=13, decimal_places=4, default=Decimal('0'), verbose_name=_('stock'))
|
|
is_digital_asset = models.BooleanField(default=False, verbose_name=_('es un activo digital'))
|
|
url = models.URLField(blank=True, verbose_name=_('URL de descarga'))
|
|
tags = models.ManyToManyField('shop.Tag', blank=True, verbose_name=_('etiquetas'))
|
|
categories = models.ManyToManyField('shop.ProductCategory', blank=True, verbose_name=_('categorías'))
|
|
brand = models.ForeignKey('shop.Brand', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('marca'))
|
|
slug = models.SlugField(default='', blank=True, null=True, max_length=128, verbose_name=_('Slug'))
|
|
hidden = models.BooleanField(default=False, db_index=True, verbose_name=_('oculto'))
|
|
is_shipping_method = models.BooleanField(default=False, verbose_name=_('es forma de envío'))
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.pk:
|
|
self.slug = slugify(self.name)[:128]
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def price(self):
|
|
return self.prices.filter(current=True).first()
|
|
|
|
class Meta:
|
|
verbose_name = _('producto')
|
|
verbose_name_plural = _('productos')
|
|
ordering = ('id',)
|
|
|
|
|
|
class ProductPrice(TimestampedModel):
|
|
price = models.DecimalField(max_digits=11, decimal_places=2, verbose_name=_('precio'))
|
|
date = models.DateTimeField(default=timezone.now, verbose_name=_('fecha'))
|
|
product = models.ForeignKey(
|
|
'shop.Product', on_delete=models.CASCADE, verbose_name=_('producto'), related_name='prices'
|
|
)
|
|
tax = models.ForeignKey('shop.Tax', on_delete=models.PROTECT, verbose_name=_('impuesto aplicable'))
|
|
price_with_tax = models.DecimalField(
|
|
max_digits=11, decimal_places=2, blank=True, null=True, verbose_name=_('precio con impuestos')
|
|
)
|
|
current = models.BooleanField(default=False, verbose_name=_('es el precio actual'))
|
|
|
|
def __str__(self):
|
|
return f'{self.price} - {self.tax.code}'
|
|
|
|
def save(self, *args, **kwargs):
|
|
tax_value = self.price * Decimal(self.tax.value / 100)
|
|
self.price_with_tax = round(self.price + tax_value, 2)
|
|
super().save()
|
|
|
|
class Meta:
|
|
verbose_name = _('precio de producto')
|
|
verbose_name_plural = _('precio de producto')
|
|
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 Meta:
|
|
verbose_name = _('imágenes de producto')
|
|
verbose_name_plural = _('imágenes de producto')
|
|
|
|
|
|
class ProductBatch(TimestampedModel):
|
|
code = models.CharField(max_length=32, unique=True, db_index=True, verbose_name=_('código'))
|
|
product = models.ForeignKey('shop.Product', on_delete=models.PROTECT, verbose_name=_('producto'))
|
|
quantity = models.DecimalField(max_digits=13, decimal_places=4, default=Decimal('1'), verbose_name=_('cantidad'))
|
|
expiration_date = models.DateField(
|
|
blank=True, null=True, verbose_name=_('fecha de caducidad / consumo preferente')
|
|
)
|
|
provider = models.ForeignKey(
|
|
'shop.Provider', on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('proveedor')
|
|
)
|
|
|
|
def __str__(self):
|
|
return f'{self.code} - {self.product.name} - {self.quantity}'
|
|
|
|
class Meta:
|
|
verbose_name = _('remesa de producto')
|
|
verbose_name_plural = _('remesas de productos')
|
|
|
|
|
|
class Tax(TimestampedModel):
|
|
code = models.CharField(max_length=8, blank=False, unique=True, verbose_name=_('código de impuesto'))
|
|
value = models.PositiveIntegerField(null=False, blank=False, verbose_name=_('valor entero (porcentaje)'))
|
|
|
|
def __str__(self):
|
|
return f'{self.code} - {self.value}%'
|
|
|
|
class Meta:
|
|
verbose_name = _('impuesto')
|
|
verbose_name_plural = _('impuestos')
|
|
ordering = ('id',)
|
|
|
|
|
|
class Provider(TimestampedModel):
|
|
vat_id = models.CharField(max_length=32, blank=False, unique=True, verbose_name=_('documento de identidad'))
|
|
name = models.CharField(max_length=64, blank=False, verbose_name=_('nombre'))
|
|
|
|
email = models.EmailField(blank=False, verbose_name=_('e-mail de contacto'))
|
|
phone = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name=_('Teléfono de contacto'))
|
|
|
|
address = models.CharField(max_length=255, blank=False, verbose_name=_('dirección'))
|
|
city = models.CharField(max_length=64, blank=False, verbose_name=_('ciudad'))
|
|
state = models.CharField(max_length=64, blank=False, verbose_name=_('región'))
|
|
country = models.CharField(max_length=64, blank=False, verbose_name=_('país'))
|
|
zip = models.CharField(max_length=32, blank=False, verbose_name=_('código postal'))
|
|
|
|
def __str__(self):
|
|
return f'{self.vat_id} - {self.name}'
|
|
|
|
class Meta:
|
|
verbose_name = _('proveedor')
|
|
verbose_name_plural = _('proveedores')
|
|
|
|
|
|
class Brand(TimestampedModel):
|
|
name = models.CharField(max_length=100, unique=True, blank=False, null=False, verbose_name=_('nombre'))
|
|
|
|
class Meta:
|
|
verbose_name = _('marca')
|
|
verbose_name_plural = _('marcas')
|
|
|
|
|
|
class OrderLine(TimestampedModel):
|
|
order = models.ForeignKey('shop.Order', on_delete=models.CASCADE, related_name='lines', verbose_name=_('pedido'))
|
|
product = models.ForeignKey(
|
|
'shop.Product', on_delete=models.CASCADE, null=False, blank=False, verbose_name=_('producto')
|
|
)
|
|
quantity = models.DecimalField(
|
|
max_digits=13, decimal_places=4, default=Decimal('1'), null=False, blank=False, verbose_name=_('cantidad')
|
|
)
|
|
|
|
# Precio del producto en el momento en el que se crea el pedido
|
|
price = models.DecimalField(max_digits=13, decimal_places=4, verbose_name=_('precio'))
|
|
|
|
# base_total = quantity * price
|
|
base_total = models.DecimalField(
|
|
max_digits=13,
|
|
decimal_places=4,
|
|
default=Decimal('0'),
|
|
null=False,
|
|
blank=False,
|
|
verbose_name=_('total sin impuestos'),
|
|
)
|
|
|
|
# Valor entero del impuesto en el momento en el que se crea el pedido
|
|
tax_value = models.PositiveIntegerField(null=False, blank=False, verbose_name=_('valor de impuestos'))
|
|
|
|
# taxes = price * quantity * (tax_value / 100)
|
|
taxes = models.DecimalField(max_digits=13, decimal_places=4, verbose_name=_('impuestos'))
|
|
|
|
# total = base_total + taxes
|
|
total = models.DecimalField(max_digits=13, decimal_places=4, verbose_name=_('total'))
|
|
|
|
def __str__(self):
|
|
return f'{self.product.name} - {self.quantity} - {self.price}'
|
|
|
|
class Meta:
|
|
verbose_name = _('línea de pedido')
|
|
verbose_name_plural = _('líneas de pedido')
|
|
|
|
|
|
def create_order_code(*args, **kwargs):
|
|
return timezone.now().isoformat().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:-5]
|
|
|
|
|
|
class Order(TimestampedModel):
|
|
class Statuses(models.TextChoices):
|
|
STATUS_PENDING = 'PEN', _('pendiente de pago')
|
|
STATUS_PAID = 'PAI', _('pagado')
|
|
STATUS_RETURNED = 'RTN', _('devuelto')
|
|
STATUS_ERROR = 'ERR', _('Error')
|
|
|
|
class ShippingStatuses(models.TextChoices):
|
|
STATUS_NOT_READY = 'NOT', _('no preparado')
|
|
STATUS_READY = 'RDY', _('preparado')
|
|
STATUS_TO_BE_SENT = 'TBS', _('listo para ser enviado')
|
|
STATUS_SENT = 'SNT', _('enviado')
|
|
STATUS_DELIVERED = 'DLV', _('entregado')
|
|
STATUS_FINISHED = 'FIN', _('finalizado')
|
|
STATUS_CANCELED = 'CAN', _('cancelado')
|
|
STATUS_REQUESTED_RETURN = 'RQT', _('devolución solicitada')
|
|
STATUS_RETURNED = 'RTN', _('devuelto')
|
|
|
|
uuid = models.UUIDField(default=uuid4, verbose_name=_('UUID'), db_index=True)
|
|
code = models.CharField(default=create_order_code, max_length=20, unique=True)
|
|
|
|
status = models.CharField(max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_('estado'))
|
|
|
|
shipping_status = models.CharField(
|
|
max_length=3, default=ShippingStatuses.STATUS_NOT_READY, verbose_name=_('estado de envío')
|
|
)
|
|
|
|
base_total = models.DecimalField(
|
|
default=Decimal('0'), max_digits=13, decimal_places=2, verbose_name=_('base imponible')
|
|
)
|
|
|
|
total = models.DecimalField(default=Decimal('0'), max_digits=13, decimal_places=2, verbose_name=_('total'))
|
|
|
|
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_('cliente'))
|
|
email = models.EmailField(blank=True, verbose_name=_('e-mail'))
|
|
|
|
# Datos de facturación
|
|
billing_address = models.CharField(max_length=255, blank=False, verbose_name=_('dirección de facturación'))
|
|
billing_city = models.CharField(max_length=64, blank=False, verbose_name=_('ciudad de facturación'))
|
|
billing_state = models.CharField(max_length=64, blank=False, verbose_name=_('región de facturación'))
|
|
billing_country = models.CharField(max_length=64, blank=False, verbose_name=_('país de facturación'))
|
|
billing_zip = models.CharField(max_length=32, blank=False, verbose_name=_('código postal de facturación'))
|
|
|
|
# Datos de envío
|
|
shipping_address = models.CharField(max_length=255, blank=False, verbose_name=_('dirección'))
|
|
shipping_city = models.CharField(max_length=64, blank=False, verbose_name=_('ciudad'))
|
|
shipping_state = models.CharField(max_length=64, blank=False, verbose_name=_('región'))
|
|
shipping_country = models.CharField(max_length=64, blank=False, verbose_name=_('país'))
|
|
shipping_zip = models.CharField(max_length=32, blank=False, verbose_name=_('código postal'))
|
|
contact_phone = models.CharField(max_length=32, blank=True, verbose_name=_('teléfono de contacto'))
|
|
|
|
amount_paid = models.DecimalField(
|
|
default=Decimal('0.00'), max_digits=11, decimal_places=2, verbose_name=_('cantidad pagada')
|
|
)
|
|
|
|
shipping_method = models.ForeignKey(
|
|
'shop.ShippingMethod', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('método de envío')
|
|
)
|
|
|
|
from_cart = models.ForeignKey(
|
|
'shop.Cart', on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('carrito de origen de pedido')
|
|
)
|
|
|
|
def calculate_total_from_lines(self):
|
|
self.base_total = self.lines.aggregate(base_total=models.Sum('base_total')).get('base_total')
|
|
self.total = self.lines.aggregate(base_total=models.Sum('total')).get('base_total')
|
|
|
|
self.save()
|
|
|
|
class Meta:
|
|
verbose_name = _('pedido')
|
|
verbose_name_plural = _('pedidos')
|
|
ordering = ('creation_date',)
|
|
|
|
|
|
class Cart(models.Model):
|
|
uuid = models.UUIDField(default=uuid4, primary_key=True, verbose_name=_('uuid'))
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('usuario'), blank=True, null=True)
|
|
creation_date = models.DateTimeField(auto_now_add=True, verbose_name=_('fecha de creación'))
|
|
|
|
class Meta:
|
|
verbose_name = _('carrito')
|
|
verbose_name_plural = _('carritos')
|
|
|
|
|
|
class CartItem(models.Model):
|
|
cart = models.ForeignKey('shop.Cart', on_delete=models.CASCADE, related_name='items', verbose_name=_('carrito'))
|
|
product = models.ForeignKey('shop.Product', on_delete=models.CASCADE, verbose_name=_('producto'))
|
|
quantity = models.PositiveIntegerField(default=1, verbose_name=_('cantidad'))
|
|
|
|
class Meta:
|
|
verbose_name = _('línea de carrito')
|
|
verbose_name_plural = _('líneas de carrito')
|
|
|
|
|
|
class CustomerAddress(models.Model):
|
|
class Types(models.TextChoices):
|
|
BILLING = 'BILL', _('facturación')
|
|
SHIPPING = 'SHIP', _('envío')
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name=_('usuario'))
|
|
vat_id = models.CharField(
|
|
max_length=16, blank=True, null=True, default='', verbose_name=_('número de identificación fiscal')
|
|
)
|
|
full_name = models.CharField(max_length=128, blank=False, null=False, verbose_name=_('dirección'))
|
|
address = models.CharField(max_length=128, blank=False, null=False, verbose_name=_('dirección'))
|
|
address_town = models.CharField(max_length=64, blank=False, null=False, verbose_name=_('localidad'))
|
|
address_zip = models.CharField(max_length=16, blank=False, null=False, verbose_name=_('código postal'))
|
|
address_state = models.CharField(max_length=64, blank=False, null=False, verbose_name=_('provincia'))
|
|
address_country = models.CharField(
|
|
max_length=64, default=settings.SHOP_COUNTRY, blank=False, null=False, verbose_name=_('país')
|
|
)
|
|
address_phone = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name=_('teléfono'))
|
|
|
|
address_type = models.CharField(
|
|
max_length=4, choices=Types.choices, default=Types.SHIPPING, verbose_name=_('tipo de dirección')
|
|
)
|
|
email = models.EmailField(blank=False, null=False, verbose_name=_('e-mail'))
|
|
default = models.BooleanField(default=True, verbose_name=_('por defecto'))
|
|
hidden = models.BooleanField(default=False, db_index=True, verbose_name=_('oculto'))
|
|
|
|
def __str__(self):
|
|
return f'{self.full_name} - {self.address}, {self.address_zip}, {self.address_state} - {self.address_country}'
|
|
|
|
class Meta:
|
|
verbose_name = _('dirección de cliente')
|
|
verbose_name_plural = _('direcciones de cliente')
|
|
|
|
|
|
class ShippingMethod(models.Model):
|
|
name = models.CharField(max_length=64, verbose_name=_('nombre'))
|
|
description = models.CharField(max_length=256, default='', blank=True, verbose_name=_('description'))
|
|
url = models.URLField(default='', blank=True, verbose_name=_('url'))
|
|
shipping_product = models.ForeignKey('shop.Product', on_delete=models.CASCADE, verbose_name=_('producto de envío'))
|
|
enabled = models.BooleanField(default=True, verbose_name=_('habilitado'))
|
|
|
|
class Meta:
|
|
verbose_name = _('método de envío')
|
|
verbose_name_plural = _('métodos de envío')
|
|
|
|
|
|
class WishlistedProduct(TimestampedModel):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('usuario'))
|
|
product = models.ForeignKey('shop.Product', on_delete=models.CASCADE, verbose_name=_('producto'))
|
|
|
|
class Meta:
|
|
verbose_name = _('productos deseados')
|
|
verbose_name_plural = _('productos deseados')
|
|
unique_together = ('user', 'product')
|
|
|
|
|
|
class Payment(models.Model):
|
|
class MethodChoices(models.TextChoices):
|
|
REDSYS = 'REDSYS'
|
|
|
|
hash = models.UUIDField(verbose_name=_('Hash'), default=uuid4, primary_key=True)
|
|
amount = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal('0'), verbose_name=_('Cantidad'))
|
|
order = models.ForeignKey(
|
|
'shop.Order', on_delete=models.PROTECT, verbose_name=_('pedido'), related_name='payments'
|
|
)
|
|
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_('cliente'))
|
|
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(
|
|
max_length=6, choices=MethodChoices.choices, default=MethodChoices.REDSYS, verbose_name=_('método de pago')
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _('Pago')
|
|
verbose_name_plural = _('Pagos')
|
|
|
|
|
|
class ShopSettings(SingletonModel):
|
|
debug = models.BooleanField(default=True, verbose_name=_('Modo depuración'))
|
|
merchant_code = models.CharField(
|
|
max_length=9, blank=False, null=False, verbose_name=_('Identificación de comercio')
|
|
)
|
|
currency_code = models.CharField(
|
|
max_length=4, default='978', choices=CURRENCY_CODES, verbose_name=_('Código de moneda')
|
|
) # 978 == EURO
|
|
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(max_length=128, blank=True, null=True, verbose_name=_('dominio del tpv'))
|
|
|
|
class Meta:
|
|
verbose_name = _('ajustes de la tienda')
|
|
verbose_name_plural = _('ajustes de la tienda')
|