fix: docker build
This commit is contained in:
+18
-12
@@ -1,20 +1,26 @@
|
|||||||
FROM python:3.13-slim
|
FROM python:3.13-alpine3.23 as builder
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
ENV UV_SYSTEM_PYTHON=1
|
ENV UV_SYSTEM_PYTHON=1
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN pip install uv && uv pip install -r pyproject.toml
|
||||||
|
|
||||||
RUN mkdir /code
|
FROM python:3.13-alpine3.23
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
COPY pyproject.toml /code
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
# Set user and group
|
||||||
apt-get install -y gcc git nginx gettext vim libpq-dev && \
|
ARG user=apps
|
||||||
pip install uv && \
|
ARG uid=1001
|
||||||
uv pip install -r pyproject.toml && \
|
ARG gid=1001
|
||||||
apt-get autoremove -y
|
RUN groupadd -g ${gid} ${user}
|
||||||
|
RUN useradd -u ${uid} -g ${user} -s /bin/sh -m ${user}
|
||||||
|
|
||||||
COPY . /code
|
RUN apk update && apk add gcc gettext vim libpq-dev
|
||||||
|
RUN chown -R ${user}:${user} /code
|
||||||
|
|
||||||
|
USER ${user}
|
||||||
|
COPY --chown=${user}:${user} . /code
|
||||||
|
|
||||||
|
RUN ["sh", "./scripts/prebuild.sh"]
|
||||||
CMD ["sh", "./scripts/run.sh"]
|
CMD ["sh", "./scripts/run.sh"]
|
||||||
|
|||||||
+31
-1
@@ -103,7 +103,10 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
|
|
||||||
LANGUAGE_CODE = 'es'
|
LANGUAGE_CODE = 'es'
|
||||||
|
|
||||||
LANGUAGES = (('en', _('Inglés')), ('es', _('Castellano')))
|
LANGUAGES = (
|
||||||
|
('en', _('Inglés')),
|
||||||
|
('es', _('Castellano')),
|
||||||
|
)
|
||||||
|
|
||||||
TIME_ZONE = 'Europe/Madrid'
|
TIME_ZONE = 'Europe/Madrid'
|
||||||
|
|
||||||
@@ -143,3 +146,30 @@ EMAIL_USE_SSL = False
|
|||||||
CELERY_BROKER_URL = env.str('CELERY_BROKER_URL', default='memory://localhost:8000//')
|
CELERY_BROKER_URL = env.str('CELERY_BROKER_URL', default='memory://localhost:8000//')
|
||||||
CELERY_TIME_ZONE = TIME_ZONE
|
CELERY_TIME_ZONE = TIME_ZONE
|
||||||
CELERY_ALWAYS_EAGER = DEBUG
|
CELERY_ALWAYS_EAGER = DEBUG
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'class': 'logging.FileHandler',
|
||||||
|
'filename': 'error.log',
|
||||||
|
'level': 'ERROR',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
},
|
||||||
|
'error': {
|
||||||
|
'handlers': ['file'],
|
||||||
|
'level': 'ERROR',
|
||||||
|
'propagate': False
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
server {
|
|
||||||
access_log /var/log/nginx/access.log;
|
|
||||||
error_log /var/log/nginx/error.log notice;
|
|
||||||
|
|
||||||
listen 80;
|
|
||||||
|
|
||||||
# Static files
|
|
||||||
location /static {
|
|
||||||
alias /code/static;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Media files
|
|
||||||
location /media {
|
|
||||||
alias /code/media;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reverse proxy
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:8000;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
}
|
|
||||||
|
|
||||||
# security headers
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|
||||||
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|
||||||
|
|
||||||
# gzip
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
|
|
||||||
}
|
|
||||||
+1
-1
@@ -6,7 +6,7 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boto3==1.34.105",
|
"boto3==1.34.105",
|
||||||
"celery==5.4.0",
|
"celery==5.6.2",
|
||||||
"django==6.0.2",
|
"django==6.0.2",
|
||||||
"django-celery-beat==2.9.0",
|
"django-celery-beat==2.9.0",
|
||||||
"django-cors-headers==3.13.0",
|
"django-cors-headers==3.13.0",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
python manage.py collectstatic
|
||||||
|
python manage.py compilemessages
|
||||||
@@ -8,9 +8,6 @@ WORKER_NUM_PROCESSES=${WORKER_NUM_PROCESSES:=1}
|
|||||||
RUN_SERVER=${RUN_SERVER:="FALSE"}
|
RUN_SERVER=${RUN_SERVER:="FALSE"}
|
||||||
RUN_CELERY=${RUN_CELERY:="FALSE"}
|
RUN_CELERY=${RUN_CELERY:="FALSE"}
|
||||||
|
|
||||||
python manage.py compilemessages
|
|
||||||
python manage.py build_tailwind_theme
|
|
||||||
|
|
||||||
if [ $RUN_SERVER = "TRUE" ]; then
|
if [ $RUN_SERVER = "TRUE" ]; then
|
||||||
uvicorn --workers $ASGI_WORKERS --host $ASGI_HOST --port $ASGI_PORT config.asgi:application
|
uvicorn --workers $ASGI_WORKERS --host $ASGI_HOST --port $ASGI_PORT config.asgi:application
|
||||||
elif [ $RUN_CELERY = "TRUE" ]; then
|
elif [ $RUN_CELERY = "TRUE" ]; then
|
||||||
|
|||||||
+56
-13
@@ -41,7 +41,11 @@ class Tag(models.Model):
|
|||||||
class ProductCategory(models.Model):
|
class ProductCategory(models.Model):
|
||||||
name = models.CharField(max_length=32)
|
name = models.CharField(max_length=32)
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
'shop.ProductCategory', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('categoría padre')
|
'shop.ProductCategory',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('categoría padre'),
|
||||||
)
|
)
|
||||||
promoted = models.BooleanField(default=False, verbose_name=_('promocionada'))
|
promoted = models.BooleanField(default=False, verbose_name=_('promocionada'))
|
||||||
hidden = models.BooleanField(default=False, verbose_name=_('oculta'))
|
hidden = models.BooleanField(default=False, verbose_name=_('oculta'))
|
||||||
@@ -101,7 +105,11 @@ class ProductPrice(TimestampedModel):
|
|||||||
)
|
)
|
||||||
tax = models.ForeignKey('shop.Tax', on_delete=models.PROTECT, verbose_name=_('impuesto aplicable'))
|
tax = models.ForeignKey('shop.Tax', on_delete=models.PROTECT, verbose_name=_('impuesto aplicable'))
|
||||||
price_with_tax = models.DecimalField(
|
price_with_tax = models.DecimalField(
|
||||||
max_digits=11, decimal_places=2, blank=True, null=True, verbose_name=_('precio con impuestos')
|
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'))
|
current = models.BooleanField(default=False, verbose_name=_('es el precio actual'))
|
||||||
|
|
||||||
@@ -329,7 +337,9 @@ class Order(TimestampedModel):
|
|||||||
status = models.CharField(max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_('estado'))
|
status = models.CharField(max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_('estado'))
|
||||||
|
|
||||||
shipping_status = models.CharField(
|
shipping_status = models.CharField(
|
||||||
max_length=3, default=ShippingStatuses.STATUS_NOT_READY, verbose_name=_('estado de envío')
|
max_length=3,
|
||||||
|
default=ShippingStatuses.STATUS_NOT_READY,
|
||||||
|
verbose_name=_('estado de envío'),
|
||||||
)
|
)
|
||||||
|
|
||||||
base_total = models.DecimalField(
|
base_total = models.DecimalField(
|
||||||
@@ -357,15 +367,25 @@ class Order(TimestampedModel):
|
|||||||
contact_phone = models.CharField(max_length=32, blank=True, verbose_name=_('teléfono de contacto'))
|
contact_phone = models.CharField(max_length=32, blank=True, verbose_name=_('teléfono de contacto'))
|
||||||
|
|
||||||
amount_paid = models.DecimalField(
|
amount_paid = models.DecimalField(
|
||||||
default=Decimal('0.00'), max_digits=11, decimal_places=2, verbose_name=_('cantidad pagada')
|
default=Decimal('0.00'),
|
||||||
|
max_digits=11, decimal_places=2,
|
||||||
|
verbose_name=_('cantidad pagada'),
|
||||||
)
|
)
|
||||||
|
|
||||||
shipping_method = models.ForeignKey(
|
shipping_method = models.ForeignKey(
|
||||||
'shop.ShippingMethod', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('método de envío')
|
'shop.ShippingMethod',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_('método de envío'),
|
||||||
)
|
)
|
||||||
|
|
||||||
from_cart = models.ForeignKey(
|
from_cart = models.ForeignKey(
|
||||||
'shop.Cart', on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('carrito de origen de pedido')
|
'shop.Cart',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('carrito de origen de pedido'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def calculate_total_from_lines(self):
|
def calculate_total_from_lines(self):
|
||||||
@@ -407,7 +427,11 @@ class CustomerAddress(models.Model):
|
|||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name=_('usuario'))
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name=_('usuario'))
|
||||||
vat_id = models.CharField(
|
vat_id = models.CharField(
|
||||||
max_length=16, blank=True, null=True, default='', verbose_name=_('número de identificación fiscal')
|
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'))
|
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 = models.CharField(max_length=128, blank=False, null=False, verbose_name=_('dirección'))
|
||||||
@@ -415,12 +439,19 @@ class CustomerAddress(models.Model):
|
|||||||
address_zip = models.CharField(max_length=16, blank=False, null=False, verbose_name=_('código postal'))
|
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_state = models.CharField(max_length=64, blank=False, null=False, verbose_name=_('provincia'))
|
||||||
address_country = models.CharField(
|
address_country = models.CharField(
|
||||||
max_length=64, default=settings.SHOP_COUNTRY, blank=False, null=False, verbose_name=_('país')
|
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_phone = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name=_('teléfono'))
|
||||||
|
|
||||||
address_type = models.CharField(
|
address_type = models.CharField(
|
||||||
max_length=4, choices=Types.choices, default=Types.SHIPPING, verbose_name=_('tipo de dirección')
|
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'))
|
email = models.EmailField(blank=False, null=False, verbose_name=_('e-mail'))
|
||||||
default = models.BooleanField(default=True, verbose_name=_('por defecto'))
|
default = models.BooleanField(default=True, verbose_name=_('por defecto'))
|
||||||
@@ -463,13 +494,19 @@ class Payment(models.Model):
|
|||||||
hash = models.UUIDField(verbose_name=_('Hash'), default=uuid4, primary_key=True)
|
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'))
|
amount = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal('0'), verbose_name=_('Cantidad'))
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
'shop.Order', on_delete=models.PROTECT, verbose_name=_('pedido'), related_name='payments'
|
'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'))
|
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'))
|
creation_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Fecha de creación'))
|
||||||
metadata = models.JSONField(default=dict, verbose_name=_('Metadata'))
|
metadata = models.JSONField(default=dict, verbose_name=_('Metadata'))
|
||||||
method = models.CharField(
|
method = models.CharField(
|
||||||
max_length=6, choices=MethodChoices.choices, default=MethodChoices.REDSYS, verbose_name=_('método de pago')
|
max_length=6,
|
||||||
|
choices=MethodChoices.choices,
|
||||||
|
default=MethodChoices.REDSYS,
|
||||||
|
verbose_name=_('método de pago'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -480,10 +517,16 @@ class Payment(models.Model):
|
|||||||
class ShopSettings(SingletonModel):
|
class ShopSettings(SingletonModel):
|
||||||
debug = models.BooleanField(default=True, verbose_name=_('Modo depuración'))
|
debug = models.BooleanField(default=True, verbose_name=_('Modo depuración'))
|
||||||
merchant_code = models.CharField(
|
merchant_code = models.CharField(
|
||||||
max_length=9, blank=False, null=False, verbose_name=_('Identificación de comercio')
|
max_length=9,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
verbose_name=_('Identificación de comercio'),
|
||||||
)
|
)
|
||||||
currency_code = models.CharField(
|
currency_code = models.CharField(
|
||||||
max_length=4, default='978', choices=CURRENCY_CODES, verbose_name=_('Código de moneda')
|
max_length=4,
|
||||||
|
default='978',
|
||||||
|
choices=CURRENCY_CODES,
|
||||||
|
verbose_name=_('Código de moneda'),
|
||||||
) # 978 == EURO
|
) # 978 == EURO
|
||||||
terminal = models.CharField(max_length=8, verbose_name=_('Terminal'))
|
terminal = models.CharField(max_length=8, verbose_name=_('Terminal'))
|
||||||
shared_secret = models.CharField(max_length=100, verbose_name=_('Clave de Redsys'))
|
shared_secret = models.CharField(max_length=100, verbose_name=_('Clave de Redsys'))
|
||||||
|
|||||||
+24
-22
@@ -7,14 +7,13 @@ from decimal import Decimal
|
|||||||
import pyDes
|
import pyDes
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Sum
|
from django.http import HttpRequest
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.text import gettext_lazy as _
|
from django.utils.text import gettext_lazy as _
|
||||||
|
|
||||||
from shop.exceptions import RedsysPaymentException, RedsysValidationException
|
from shop.exceptions import RedsysPaymentException, RedsysValidationException
|
||||||
from shop.models import Cart, CartItem, Order, OrderLine, Payment, Product, ProductBatch, ProductPrice, ShippingMethod
|
from shop.models import Cart, CartItem, Order, OrderLine, Payment, Product, ProductBatch, ProductPrice, ShippingMethod
|
||||||
from shop.settings import ERROR_CODES
|
from shop.settings import ERROR_CODES
|
||||||
from shop.signals import clear_cart
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ def create_order(
|
|||||||
|
|
||||||
def delete_product_batch(batch: ProductBatch):
|
def delete_product_batch(batch: ProductBatch):
|
||||||
product = batch.product
|
product = batch.product
|
||||||
product.stock = max(product.stock - batch.quantity, 0)
|
product.stock = max(product.stock - batch.quantity, Decimal('0.00'))
|
||||||
product.save()
|
product.save()
|
||||||
batch.delete()
|
batch.delete()
|
||||||
|
|
||||||
@@ -77,20 +76,20 @@ def delete_product_batch(batch: ProductBatch):
|
|||||||
def create_order_from_cart(
|
def create_order_from_cart(
|
||||||
cart: Cart,
|
cart: Cart,
|
||||||
shipping_method: ShippingMethod,
|
shipping_method: ShippingMethod,
|
||||||
billing_address_full_name='',
|
billing_address_full_name: str = '',
|
||||||
billing_address_address='',
|
billing_address_address: str = '',
|
||||||
billing_address_town='',
|
billing_address_town: str = '',
|
||||||
billing_address_state='',
|
billing_address_state: str = '',
|
||||||
billing_address_country='',
|
billing_address_country: str = '',
|
||||||
billing_address_zip='',
|
billing_address_zip: str = '',
|
||||||
shipping_address_full_name='',
|
shipping_address_full_name: str = '',
|
||||||
shipping_address_address='',
|
shipping_address_address: str = '',
|
||||||
shipping_address_town='',
|
shipping_address_town: str = '',
|
||||||
shipping_address_state='',
|
shipping_address_state: str = '',
|
||||||
shipping_address_country='',
|
shipping_address_country: str = '',
|
||||||
shipping_address_zip='',
|
shipping_address_zip: str = '',
|
||||||
shipping_address_phone='',
|
shipping_address_phone: str = '',
|
||||||
email='',
|
email: str = '',
|
||||||
):
|
):
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
billing_address=f'{billing_address_full_name} {billing_address_address}',
|
billing_address=f'{billing_address_full_name} {billing_address_address}',
|
||||||
@@ -155,7 +154,7 @@ def add_shipping_order_line(order, shipping_method):
|
|||||||
order.calculate_total_from_lines()
|
order.calculate_total_from_lines()
|
||||||
|
|
||||||
|
|
||||||
def compute_signature(salt, payload, key):
|
def compute_signature(salt: str, payload: str, key: str) -> bytes:
|
||||||
"""
|
"""
|
||||||
:param salt: order number (Ds_Order or Ds_Merchant_Order)
|
:param salt: order number (Ds_Order or Ds_Merchant_Order)
|
||||||
:param payload: Ds_MerchantParameters
|
:param payload: Ds_MerchantParameters
|
||||||
@@ -171,7 +170,7 @@ def compute_signature(salt, payload, key):
|
|||||||
return base64.b64encode(payload_hash)
|
return base64.b64encode(payload_hash)
|
||||||
|
|
||||||
|
|
||||||
def validate_payment_for_order(request, order: Order) -> Decimal:
|
def validate_payment_for_order(request: HttpRequest, order: Order) -> Decimal:
|
||||||
"""
|
"""
|
||||||
example_response_data = {
|
example_response_data = {
|
||||||
'Ds_MerchantCode': '999008881',
|
'Ds_MerchantCode': '999008881',
|
||||||
@@ -262,15 +261,18 @@ def update_order_payment_status(order: Order):
|
|||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
|
|
||||||
def delete_cart_items_from_order(order):
|
def delete_cart_items_from_order(order: Order):
|
||||||
if order.status == Order.Statuses.STATUS_PAID and order.from_cart:
|
if order.status == Order.Statuses.STATUS_PAID and order.from_cart:
|
||||||
CartItem.objects.filter(cart=order.from_cart).delete()
|
CartItem.objects.filter(cart=order.from_cart).delete()
|
||||||
|
|
||||||
|
|
||||||
def add_payment_to_order(order: Order, amount):
|
def add_payment_to_order(order: Order, amount: Decimal):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
payment = Payment.objects.create(
|
payment = Payment.objects.create(
|
||||||
amount=amount, order=order, user=order.user, method=Payment.MethodChoices.REDSYS
|
amount=amount,
|
||||||
|
order=order,
|
||||||
|
user=order.user,
|
||||||
|
method=Payment.MethodChoices.REDSYS,
|
||||||
)
|
)
|
||||||
order.amount_paid += payment.amount
|
order.amount_paid += payment.amount
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "celery"
|
name = "celery"
|
||||||
version = "5.4.0"
|
version = "5.6.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "billiard" },
|
{ name = "billiard" },
|
||||||
@@ -110,12 +110,12 @@ dependencies = [
|
|||||||
{ name = "click-repl" },
|
{ name = "click-repl" },
|
||||||
{ name = "kombu" },
|
{ name = "kombu" },
|
||||||
{ name = "python-dateutil" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "tzdata" },
|
{ name = "tzlocal" },
|
||||||
{ name = "vine" },
|
{ name = "vine" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692 }
|
sdist = { url = "https://files.pythonhosted.org/packages/8f/9d/3d13596519cfa7207a6f9834f4b082554845eb3cd2684b5f8535d50c7c44/celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b", size = 1718802 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983 },
|
{ url = "https://files.pythonhosted.org/packages/dd/bd/9ecd619e456ae4ba73b6583cc313f26152afae13e9a82ac4fe7f8856bfd1/celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5", size = 445502 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -550,18 +550,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f9/04/da4d140d601609f1afd76c036be0b434165d701c1b9b660db895b4fa1d2f/django_watchman-1.3.0-py2.py3-none-any.whl", hash = "sha256:5f04300bd7fbdd63b8a883b2730ed1e4d9b0f9991133b33a1281134b81f466eb", size = 20090 },
|
{ url = "https://files.pythonhosted.org/packages/f9/04/da4d140d601609f1afd76c036be0b434165d701c1b9b660db895b4fa1d2f/django_watchman-1.3.0-py2.py3-none-any.whl", hash = "sha256:5f04300bd7fbdd63b8a883b2730ed1e4d9b0f9991133b33a1281134b81f466eb", size = 20090 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "djangorestframework"
|
|
||||||
version = "3.15.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "django" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "executing"
|
name = "executing"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -975,7 +963,6 @@ dependencies = [
|
|||||||
{ name = "django-storages" },
|
{ name = "django-storages" },
|
||||||
{ name = "django-unfold" },
|
{ name = "django-unfold" },
|
||||||
{ name = "django-watchman" },
|
{ name = "django-watchman" },
|
||||||
{ name = "djangorestframework" },
|
|
||||||
{ name = "gonk" },
|
{ name = "gonk" },
|
||||||
{ name = "ipython" },
|
{ name = "ipython" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
@@ -1003,7 +990,7 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "boto3", specifier = "==1.34.105" },
|
{ name = "boto3", specifier = "==1.34.105" },
|
||||||
{ name = "celery", specifier = "==5.4.0" },
|
{ name = "celery", specifier = "==5.6.2" },
|
||||||
{ name = "dj-database-url", specifier = "==1.0.0" },
|
{ name = "dj-database-url", specifier = "==1.0.0" },
|
||||||
{ name = "django", specifier = "==6.0.2" },
|
{ name = "django", specifier = "==6.0.2" },
|
||||||
{ name = "django-celery-beat", specifier = "==2.9.0" },
|
{ name = "django-celery-beat", specifier = "==2.9.0" },
|
||||||
@@ -1016,7 +1003,6 @@ requires-dist = [
|
|||||||
{ name = "django-storages", specifier = "==1.13.2" },
|
{ name = "django-storages", specifier = "==1.13.2" },
|
||||||
{ name = "django-unfold", specifier = "==0.42.0" },
|
{ name = "django-unfold", specifier = "==0.42.0" },
|
||||||
{ name = "django-watchman", specifier = "==1.3.0" },
|
{ name = "django-watchman", specifier = "==1.3.0" },
|
||||||
{ name = "djangorestframework", specifier = "==3.15.2" },
|
|
||||||
{ name = "gonk", specifier = "==0.6.1" },
|
{ name = "gonk", specifier = "==0.6.1" },
|
||||||
{ name = "ipython", specifier = "==8.16.1" },
|
{ name = "ipython", specifier = "==8.16.1" },
|
||||||
{ name = "pillow", specifier = "==11.0.0" },
|
{ name = "pillow", specifier = "==11.0.0" },
|
||||||
@@ -1091,6 +1077,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 },
|
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzlocal"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.6.3"
|
version = "2.6.3"
|
||||||
|
|||||||
+3
-1
@@ -43,4 +43,6 @@ class StylingMixin:
|
|||||||
field.widget.attrs.update({'class': ' '.join(self.classes)})
|
field.widget.attrs.update({'class': ' '.join(self.classes)})
|
||||||
|
|
||||||
if self.placeholder_for_field.get(field_name):
|
if self.placeholder_for_field.get(field_name):
|
||||||
field.widget.attrs.update({'placeholder': self.placeholder_for_field.get(field_name)})
|
field.widget.attrs.update({
|
||||||
|
'placeholder': self.placeholder_for_field.get(field_name),
|
||||||
|
})
|
||||||
|
|||||||
@@ -52,8 +52,6 @@
|
|||||||
--leading-tight: 1.25;
|
--leading-tight: 1.25;
|
||||||
--radius-md: 0.375rem;
|
--radius-md: 0.375rem;
|
||||||
--radius-lg: 0.5rem;
|
--radius-lg: 0.5rem;
|
||||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
|
||||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--animate-spin: spin 1s linear infinite;
|
--animate-spin: spin 1s linear infinite;
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|||||||
+16
-7
@@ -10,7 +10,10 @@ from django.views.generic import TemplateView
|
|||||||
|
|
||||||
from config.mixins import HTMXFormComponent
|
from config.mixins import HTMXFormComponent
|
||||||
from shop.filters import ProductFilter
|
from shop.filters import ProductFilter
|
||||||
from shop.models import CartItem, CustomerAddress, Product, ProductPrice, ShippingMethod, WishlistedProduct
|
from shop.models import (
|
||||||
|
CartItem, CustomerAddress, Product, ProductPrice,
|
||||||
|
ShippingMethod, WishlistedProduct,
|
||||||
|
)
|
||||||
from users.forms.change_password import ChangePasswordForm
|
from users.forms.change_password import ChangePasswordForm
|
||||||
from users.forms.info import UserInfoForm
|
from users.forms.info import UserInfoForm
|
||||||
from web.forms import CustomerAddressForm
|
from web.forms import CustomerAddressForm
|
||||||
@@ -140,7 +143,11 @@ class UserInfoFormComponentView(HTMXFormComponent):
|
|||||||
return reverse('web:user_info_component')
|
return reverse('web:user_info_component')
|
||||||
|
|
||||||
def get_initial_values(self, instance):
|
def get_initial_values(self, instance):
|
||||||
return {'email': instance.email, 'first_name': instance.first_name, 'last_name': instance.last_name}
|
return {
|
||||||
|
'email': instance.email,
|
||||||
|
'first_name': instance.first_name,
|
||||||
|
'last_name': instance.last_name
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordFormComponentView(HTMXFormComponent):
|
class ChangePasswordFormComponentView(HTMXFormComponent):
|
||||||
@@ -165,18 +172,20 @@ class ListCustomerAddressComponent(TemplateView):
|
|||||||
template_name = 'components/users/retrieve_update_address.html'
|
template_name = 'components/users/retrieve_update_address.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
return {'customer_addresses': CustomerAddress.objects.filter(user=self.request.user)}
|
return {
|
||||||
|
'customer_addresses': CustomerAddress.objects.filter(user=self.request.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def update_customer_address(request, pk):
|
def update_customer_address(request, pk):
|
||||||
customer_address = get_object_or_404(CustomerAddress, pk=pk, user=request.user)
|
customer_address = get_object_or_404(CustomerAddress, pk=pk, user=request.user)
|
||||||
form = CustomerAddressForm(request.POST, instance=customer_address)
|
form = CustomerAddressForm(request.POST, instance=customer_address)
|
||||||
|
|
||||||
if form.is_valid():
|
if not form.is_valid():
|
||||||
form.save()
|
return HttpResponse(form.errors, status=400)
|
||||||
return HttpResponse(status=200, headers={'HX-Trigger': 'updated_addresses'})
|
|
||||||
|
|
||||||
return HttpResponse(form.errors, status=400)
|
form.save()
|
||||||
|
return HttpResponse(status=200, headers={'HX-Trigger': 'updated_addresses'})
|
||||||
|
|
||||||
|
|
||||||
list_products = ListProducts.as_view()
|
list_products = ListProducts.as_view()
|
||||||
|
|||||||
Reference in New Issue
Block a user