From 162b63cae98ea0df0b7d39f55c6317e65192997a Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 10 Mar 2026 13:08:10 +0100 Subject: [PATCH] feat: Add user purchase and receipt management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented 'Mis Compras' and 'Mis Recibos' pages for users to view their orders and payment receipts. - Enhanced address validation in 'editar_direccion.html' to ensure cities and postal codes belong to Almería. - Added shipping address display in seller order details on 'pedidos_vendedor.html'. - Updated user portal to include links to purchases and receipts. - Introduced email verification functionality during user registration. - Refactored email sending utility for better error handling and logging. - Improved session management for checkout processes with selected shipping addresses. --- proyecto/__init__.py | 2 + proyecto/__pycache__/__init__.cpython-314.pyc | Bin 169 -> 243 bytes proyecto/__pycache__/settings.cpython-314.pyc | Bin 4554 -> 10039 bytes proyecto/settings.py | 168 ++++++++- tienda/__pycache__/admin.cpython-314.pyc | Bin 4544 -> 4731 bytes tienda/__pycache__/models.cpython-314.pyc | Bin 14436 -> 16225 bytes tienda/__pycache__/urls.cpython-314.pyc | Bin 4211 -> 4523 bytes tienda/__pycache__/vars.cpython-314.pyc | Bin 213 -> 2214 bytes tienda/__pycache__/views.cpython-314.pyc | Bin 52321 -> 68783 bytes tienda/admin.py | 6 +- tienda/migrations/0001_initial.py | 161 +++++++- ...category_options_remove_category_parent.py | 21 -- ...002_verificationcode_code_mode_and_more.py | 23 ++ tienda/migrations/0003_image.py | 21 -- tienda/migrations/0004_alter_image_image.py | 18 - tienda/migrations/0005_product.py | 25 -- .../0006_product_secondary_images.py | 18 - tienda/migrations/0007_product_briefdesc.py | 18 - tienda/migrations/0008_cart_cartitem.py | 39 -- tienda/migrations/0009_product_creator.py | 21 -- tienda/migrations/0010_order_orderitem.py | 45 --- tienda/migrations/0011_ordermessage.py | 29 -- .../0012_image_alt_shippingaddress.py | 43 --- .../__pycache__/0001_initial.cpython-314.pyc | Bin 1344 -> 12698 bytes ...ons_remove_category_parent.cpython-314.pyc | Bin 815 -> 0 bytes ...ioncode_code_mode_and_more.cpython-314.pyc | Bin 0 -> 1071 bytes .../__pycache__/0003_image.cpython-314.pyc | Bin 1074 -> 0 bytes .../0004_alter_image_image.cpython-314.pyc | Bin 791 -> 0 bytes .../__pycache__/0005_product.cpython-314.pyc | Bin 1652 -> 0 bytes ...6_product_secondary_images.cpython-314.pyc | Bin 873 -> 0 bytes .../0007_product_briefdesc.cpython-314.pyc | Bin 811 -> 0 bytes .../0008_cart_cartitem.cpython-314.pyc | Bin 2476 -> 0 bytes .../0009_auto_20260206_1042.cpython-314.pyc | Bin 583 -> 0 bytes .../0009_product_creator.cpython-314.pyc | Bin 1249 -> 0 bytes .../0010_order_orderitem.cpython-314.pyc | Bin 3646 -> 0 bytes .../0011_ordermessage.cpython-314.pyc | Bin 1869 -> 0 bytes ..._image_alt_shippingaddress.cpython-314.pyc | Bin 2819 -> 0 bytes tienda/models.py | 28 +- tienda/static/js/checkout.js | 12 +- tienda/tasks.py | 7 + tienda/templates/tienda/base.html | 47 +-- tienda/templates/tienda/checkout.html | 42 ++- tienda/templates/tienda/editar_direccion.html | 78 +++- tienda/templates/tienda/mis_compras.html | 63 ++++ tienda/templates/tienda/mis_recibos.html | 63 ++++ tienda/templates/tienda/pedidos_vendedor.html | 16 + tienda/templates/tienda/portal_usuario.html | 15 + tienda/urls.py | 3 + tienda/utilities.py | 46 +++ tienda/vars.py | 42 ++- tienda/views.py | 347 ++++++++++++++++-- 51 files changed, 1082 insertions(+), 385 deletions(-) delete mode 100644 tienda/migrations/0002_alter_category_options_remove_category_parent.py create mode 100644 tienda/migrations/0002_verificationcode_code_mode_and_more.py delete mode 100644 tienda/migrations/0003_image.py delete mode 100644 tienda/migrations/0004_alter_image_image.py delete mode 100644 tienda/migrations/0005_product.py delete mode 100644 tienda/migrations/0006_product_secondary_images.py delete mode 100644 tienda/migrations/0007_product_briefdesc.py delete mode 100644 tienda/migrations/0008_cart_cartitem.py delete mode 100644 tienda/migrations/0009_product_creator.py delete mode 100644 tienda/migrations/0010_order_orderitem.py delete mode 100644 tienda/migrations/0011_ordermessage.py delete mode 100644 tienda/migrations/0012_image_alt_shippingaddress.py delete mode 100644 tienda/migrations/__pycache__/0002_alter_category_options_remove_category_parent.cpython-314.pyc create mode 100644 tienda/migrations/__pycache__/0002_verificationcode_code_mode_and_more.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0003_image.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0004_alter_image_image.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0005_product.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0006_product_secondary_images.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0007_product_briefdesc.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0008_cart_cartitem.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0009_auto_20260206_1042.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0009_product_creator.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0010_order_orderitem.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0011_ordermessage.cpython-314.pyc delete mode 100644 tienda/migrations/__pycache__/0012_image_alt_shippingaddress.cpython-314.pyc create mode 100644 tienda/tasks.py create mode 100644 tienda/templates/tienda/mis_compras.html create mode 100644 tienda/templates/tienda/mis_recibos.html create mode 100644 tienda/utilities.py diff --git a/proyecto/__init__.py b/proyecto/__init__.py index e69de29..a289a61 100644 --- a/proyecto/__init__.py +++ b/proyecto/__init__.py @@ -0,0 +1,2 @@ +from .celery import app as celery_app +__all__ = ('celery_app',) \ No newline at end of file diff --git a/proyecto/__pycache__/__init__.cpython-314.pyc b/proyecto/__pycache__/__init__.cpython-314.pyc index 68e01f01ba1c34bf1247cb26bee724ecd4868f6d..3adf1a098fe65e214eba314d02e1d5dc7d78ddbe 100644 GIT binary patch delta 167 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 XYWNBbzZsXVknN3i{AGV9MS1=L95Y4f diff --git a/proyecto/settings.py b/proyecto/settings.py index 6e9f179..913eab2 100644 --- a/proyecto/settings.py +++ b/proyecto/settings.py @@ -10,26 +10,65 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/6.0/ref/settings/ """ +import logging +import os, sys from pathlib import Path + +def load_dotenv(dotenv_path: Path) -> None: + if not dotenv_path.exists(): + return + + for raw_line in dotenv_path.read_text(encoding='utf-8').splitlines(): + line = raw_line.strip() + if not line or line.startswith('#') or '=' not in line: + continue + + key, value = line.split('=', 1) + key = key.strip() + value = value.strip().strip('"').strip("'") + os.environ.setdefault(key, value) + + +def env_bool(name: str, default: bool = False) -> bool: + value = os.getenv(name) + if value is None: + return default + return value.strip().lower() in {'1', 'true', 'yes', 'on'} + + +def env_list(name: str, default: list[str] | None = None) -> list[str]: + value = os.getenv(name) + if value is None: + return default or [] + return [item.strip() for item in value.split(',') if item.strip()] + + +def env_int(name: str, default: int) -> int: + value = os.getenv(name) + if value is None: + return default + return int(value) + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +load_dotenv(BASE_DIR / '.env') # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-#g((q@lvnkt(j6)2(gvtn0px)r2r(911)pv59i(6w)5e!_-^ao' +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-#g((q@lvnkt(j6)2(gvtn0px)r2r(911)pv59i(6w)5e!_-^ao') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = env_bool('DEBUG', True) -ALLOWED_HOSTS = [ - "192.168.1.142", - "localhost", - "127.0.0.1" -] +ALLOWED_HOSTS = env_list('ALLOWED_HOSTS', [ + '192.168.1.142', + 'localhost', + '127.0.0.1', +]) # Application definition @@ -152,7 +191,7 @@ MEDIA_ROOT = BASE_DIR / 'tienda' / 'static' / 'media' CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://127.0.0.1:6379/1', + 'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'), 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } @@ -177,11 +216,114 @@ MESSAGE_TAGS = { # Login URL LOGIN_URL = '/tienda/login/' -STRIPE_PUBLISHABLE_KEY = 'pk_test_51SxmSYJ2DN4I0upQDdiPeda51nmpB0ZEWfkNFKHhWBG4knIgtRoC1d9iFRoxRNdJKiLlQsIddlebU06R9XCfiSZH00ffoirwPw' -STRIPE_SECRET_KEY = 'sk_test_51SxmSYJ2DN4I0upQZb42dWKuIKToZxkQeK3vsCdijcaUr17EMEyFcLdIAm5AVEvUs96MAxl4KnZ4Yncp5VykO4ej00MZGs6c1F' +STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', '') +STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '') # PayPal Configuration (Sandbox) # Para obtener credenciales: https://sandbox.paypal.com/ -PAYPAL_CLIENT_ID = 'AX3TIklQ41456StP2puciDfkQ6oSWAQWNYB8H9ThDsU6C_VYhWqwDZ1w0dK-No38Aa9IqAbrZbE-1kHJ' # Reemplazar con tu Client ID de PayPal Sandbox -PAYPAL_CLIENT_SECRET = 'EIXny9EkiebiCnwkfmWJa7ufwHwdUCTeSZ5TiUZycBPREradcN7U0vBKCUlg-PYd3SeXTW33D0kZb5BT' # Reemplazar con tu Client Secret de PayPal Sandbox -PAYPAL_MODE = 'sandbox' # Cambiar a 'live' en producción +PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', '') # Reemplazar con tu Client ID de PayPal Sandbox +PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', '') # Reemplazar con tu Client Secret de PayPal Sandbox +PAYPAL_MODE = os.getenv('PAYPAL_MODE', 'sandbox') # Cambiar a 'live' en producción + + +SMTP_ENDPOINT = os.getenv('SMTP_ENDPOINT', 'smtp.email.eu-paris-1.oci.oraclecloud.com') +SMTP_PORT = env_int('SMTP_PORT', 587) +SECURITY = os.getenv('SECURITY', 'tls') +SMTP_USERNAME = os.getenv('SMTP_USERNAME', None) +SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', None) +SMTP_EMAIL = os.getenv("SMTP_EMAIL", None) +if SMTP_USERNAME is None or SMTP_PASSWORD is None or SMTP_EMAIL is None: + print("Se requieren credenciales SMTP") + sys.exit(1) + + + +AUTH_USER_MODEL = 'tienda.User' + + +DOMAIN = os.getenv("DOMAIN", "localhost") +PROTOCOL = os.getenv("PROTOCOL", "http") + + +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper() +LOG_DIR = Path(os.getenv('LOG_DIR', BASE_DIR / 'logs')) +LOG_DIR.mkdir(parents=True, exist_ok=True) +LOG_FILE = LOG_DIR / os.getenv('LOG_FILE', 'app.log') + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '%(asctime)s | %(levelname)s | %(name)s | %(message)s', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'standard', + 'level': LOG_LEVEL, + }, + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'standard', + 'filename': str(LOG_FILE), + 'maxBytes': 5 * 1024 * 1024, + 'backupCount': 5, + 'level': LOG_LEVEL, + 'encoding': 'utf-8', + }, + }, + 'loggers': { + 'tienda': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'tienda.audit': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + 'email.system': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + 'propagate': False + } + }, +} + + +logging.captureWarnings(True) + + + + + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = SMTP_ENDPOINT +EMAIL_PORT = SMTP_PORT +EMAIL_USE_TLS = (SECURITY == 'tls') # True si SECURITY es 'tls' +EMAIL_USE_SSL = (SECURITY == 'ssl') # True si SECURITY es 'ssl' +EMAIL_HOST_USER = SMTP_USERNAME +EMAIL_HOST_PASSWORD = SMTP_PASSWORD + +# El correo que se usará como remitente por defecto +DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", SMTP_EMAIL) + +# URL de Redis (asumiendo que corre en el puerto default 6379) +CELERY_BROKER_URL = 'redis://localhost:6379/0' + +# Opcional: para guardar el resultado de las tareas +CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' + +# Configuraciones adicionales recomendadas +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' \ No newline at end of file diff --git a/tienda/__pycache__/admin.cpython-314.pyc b/tienda/__pycache__/admin.cpython-314.pyc index b2989732e1522457ca5d163c59bc4e88bf47b494..1b0d6563aa5156323a943f6a27323edeac0293a5 100644 GIT binary patch delta 1565 zcma)6-D?zA6ra1ZGrO6Y%}jQa{UA-6xY=YU(QUN0tWsO6F~pC27)?b~mNk2mj_wzC zCQw=sd=jM4hSL|H1RwfRum%4Beb9GX0_uQ@Z;CG#`k?sYJ$I7m>Ovic-<*5yIrsj4 z_nbSovag3?W1TTg;?JE2e>!Wcu}o-6Ud?~0P3fLKWq8J?58(MdZC5hh2K7jT(!89K=EHCy)USHh88Y_?jFU041^yyg zCB8oHfh!~#`1FKG>flLQkkq{ylnSi}DLtFM^P9fprtjv4FO{0_%1Uuaqk_ zr*3Ot$%oQ@c)_L2x4xsyS#lQ3p3|uBZ&WD!q9vgzUm+G~;UlWaQ<({;!x=Rlm}V>k zzlHx%GdS#nseOYJ?{~T1H<(wdP^f zk%BJv@5OO1{1llCxc7n?8-;%(mqv#83_lQKnR+F6<*X;jeZYS=T#fEa3;xk#8&MWd zov~GxhgZ?Q_-;P(qvcA4R!U{hCTuU{JGM=~F9L~thA@t>9RV}h=Lji;od{oWaIq2h z1_xL~XA!P-Y!3t!;b$$wi(>-OMh*l_q92?XCgI&+JqSqzu?5C6QABY%6F8iJnzmlq z3BLI8j$qLUm-kHD6UXw{GCPQ&hY-e~sOM%5qg!jc@p~%8COa^*9fe7pn1ny{;XvP{ z(Dy>$wXmTr&m(-L!798}jV5bURu;Wdye6bJGWHEd=%Rw9CUX{Pz*aC1y<^Ml zC|oec_k2WY+EJZFsYk@n{W|)wy1F8n+ZWNbr$;E0`*kR`}h;V_>JC+Z_0q zeiN(xPrseO9Z$eTbB7`N1jjRW8g7~gBFw%T{sZzNB2zE^7Cr#NSfl Z*NHVL)8e_?J+vm}{tLlv>tz@@zX8K^JSPAE delta 1485 zcma)6Pi)gx7`NlZcAPj(Len&KDk-5^-H6b3QWnr!v8Wv|NL!Uw)25=x)Nh?3amup; zViHho)u!!MebNqa;lPeNOdyWakapT(2S5TDp`9n~umgumyX<|>jM5SWmhy|=_vgLe z@B6;ze-Hdx&`Swj6W9~^@jLtKrk)Eu3~i27_U;qTi1U5u*!j%oHrIzT-5qyd=lFY2 z%Lqflf|L~&!f;gVgJnsTy#za=A8v^y_*xu*q!f)Lxn*nH5=-&KC`;6~6Vp8GVrV;Y z54ZgOlBJhh2BjA<*~~(!AQ><|5z}hfj-kSa^f7#)4MQyay_kR>!l#CH7C{lP zGH#r~H^Sq1@A}N+xV|w%Q9ae+hMt3btPkIvJ!)mib{F5FXV@RgiP zzr*Zqh(WPPd3XVSlB?r9JhIsK%qC4R@2sKtA=~o99hXo)<`UPn+fL&=xl9L`n--u? z8I%ot9fc!GAv?lejRfsMOA;Z4kOogF_aJ{?v7M&v5SQ`$S;@);bZ6iXr5xila=6Zc zqJAWEQ8^}R6t1Y>zPq1!SdrqZ#A~>nvmWR8ii|3hqP1gOhI;z{s6dHKi%1~6Vp7jn z)`F7w>V=N!c(!+$R?(}PmPM?F=@}xOf;-WJ_1#jC;6a2k!XX6Aq~iz~gb9TA7`P&X zqd|QuXske48xMq4fM|18z)XFmUKot6rh5>25%|^^A9R~ntA2 zVl+5Np+)G#Cezq3L!!qp{WyXFzr}`UPoP=rN@ka0e()G(cB3_oj%j#5UJSHO!%BSN zIR%wb_$5A6nq)D4jCZlIM(q_6u;I)?FTG224m5pi@-5oXjY=J<)Zvmo7*MI-zM=Pv zBcCwy?3YrxTsA_!)Fh6twAgH$tAy?tDR`1tTjJBC)QRJo=g9MG5ebG#Yg3ERhV7A- zFT2EH3;G!z$#saqpX4V4SEGT~$T3@lehN2x@^3R+e785u))Jn$1bT8J&MytFXN^8M znVf;^$^7U1;%W7^MVcStCx-G{#wU_DlJ|>Oitn^@$b9vK7W;ZM$rN2>`{GEuHHQC4 eq9{HU@_!1;kA&q1!ZPGj)zC8;uBEoZNaH_mIwXGp diff --git a/tienda/__pycache__/models.cpython-314.pyc b/tienda/__pycache__/models.cpython-314.pyc index d30e9f691bf1a4f64df38f6fe7183a8209c3fb28..e656c0b5d9ede24288d87ee6ccd1c1fd146c0e1c 100644 GIT binary patch delta 5752 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 VYUdL>7ddDycF^WK)(d#D{{jJU6~h1k delta 4293 zcmai1du)@}75}auiS5|29l!E$5+~&4guE%?5gs9sCMtx)2^6XZ&)DA*7sn3w`y5Dl zgjEcIh6dZ2G_h8KXO%dW2>e;{-J-Y+9Z(HPebY+OszVptBSUD(;nyC&q+)Y zM5O$D@44rmd+#~Fd(Qp-aQ4qL#Vy4Sn}mPs9FY8H@LR>Dn|{b7>6~;#im)MRkg2Si z+J+&+piwnqn?;O6ra`l6u9kdKjTA8jq=+&OEQa)Sx{zOl39h! zhO(k1nN7&-D03{yiiE5fWzHp;UC3N0b1%spLRNw@Pfk{2^lBYoq0;7dy*D}Wm7Ub|+Sj-8CA&Y%NYFDHSe40ZiGN3Ou3@QW%jRZw_=WAw z5=~k#a0^*0Ovqo2Wm76l7qz-b!fQ!vKAMW9kHL?N`r{OBUPRzPP%zk10~afa?Kp;} zW0UgYVceuwLLk=lVrw|;;69T02?7MA1av}PMqr0wdoWDacsW4@K@dSKwo_lMnuG*) z0Q;zZtZF!IE(4>(&AfQ57w^34NV545Db^q?FmaXzrdc!TC19>$(AmH|kZBBn$92))L$=HW`R;~yUEQn( z9=qyM_4BR()Vh=IEvvNNa<{O0xbLpnlIQ;5^W0AxwT{atRY5hVEU9m%U1+v}440FW z>6hd=!78n=zi+I+Z}%R}63>yCS3|61xLd@om$tKm;Mq}JPs0RW_+!a^J&Nm*oXxBm z?@twc>3I;O_$<40!6=BD6Tm0lTRoa7i#Jd!Qe;I*VrY-y{_>qg^948U{V}dCQnLd- z@Vy`${RUj{4>KD)^fxjm{KwzPHb8rz&z^4sP6w)3H@q2W%*93ve5Yb7S)L?V9iU64 z9c&}~q_im)pnsK~Xn&1V^Q}^!JfwNV&L(9wIvZ1?vFS`Ut!{xEWp!e;`|Ie^A#p9o zv$3?AROk2+6etFShLCfymY?11~6v=|8vET>5_r_EooF)aRC_*EBEj^8oy%CfxlE zQeU}*;AmiJt55Uv0PL!5n9t*te=X1I`CafzP(kqOYqXfi0puplJCae7YI0T{N@Gow zbEQrkdXB~|u|xvXa7^XrX`*1EINoImjXW>coS``cyTG$g8wzt%w_x%bi+bd@6$#ND=jrhj18KCW<|hTd(+hlv-n0hRUGIm)ui0 zA}5lGOo$)UwO_^`d=+Hp@QNHJ)s>VH>~`$u37IcuTopDq>KXTbP`o0J)5JA0*;rHS z{7D*939;0D1ceB7$`U%=+;)=*O} zU>XZ!aOnur8^1@5(VSLS)IdX@K}puK1G9a@^M2N&)V zhoj4_nZ*M7^%njUsZjXuXQWP!)e00jl|l|(C5gD{yWyMdm7CtB{<3d{Tc2HA?vgga zsXKpaH!|Imcj2!c%?xstr5{dq-mDOQ)EtVOR-@DST)?NQ!i5{DwluM8@LE?xOoUS} z8*5hy-FM0KBX$*wUp;fmC z&UBZtx8S?ojh%U+@U994Ex8;7;g13MtlKvtJpUUS?nBTDbEnSpt=R5!9Q-~RAOINO za6q@}b6ZQp6c}y^okri*a-$QqA9eV_adfUUa zvpg<{9JWk|$26*6JGkBJ3w=!eN(Axr+?o3ndq`4(8}PXI-*&M}>JEop@aeYBdCfE% z`R& zM#k79Dfmcgc_fuTDKN3Ji?dfwUpfsBw!i4AVmmGlT-keR?-L1|Pj*4ejxH1J^#9sX BtVaL< diff --git a/tienda/__pycache__/urls.cpython-314.pyc b/tienda/__pycache__/urls.cpython-314.pyc index a5269ea6bfe9b751ccf84b7710433650386bdfff..ab71d24a1c7947e8e67928048baad7dc5c6d154d 100644 GIT binary patch delta 1096 zcmZvb&rcIU6vw-C3PNeiuS(e#gb*kNmTiSXEof+vAJS4_p&$^V2*or3id#?w48~JE z*=7$dCpk9pU=0_J-o5N5lWZg*#{Xc9@uC-JW|2r>lHJVve!lbGo0r|6^&c9|ZDv!6 z4zEr1W6W2YHrtrwCudRN!6MV36GDsni7=s;moO$6di0tWW~eiaofDT`BofB2I47`E zxW+m8zjLdoRS+6=uBqeh!e9EkW!s7EB{3G~m*eZ3;!=V?HLk}JkItjr3oJ&LV^8A$ z@OgCHS`uXHcFUhf#n|dgzAv#Q`j_J?(R-(bniO`mNjt_brD7XyOKc}EPUhI?<8UMIhu|J6;v~jYZ6mY<(_~F*FcV6Lj&>k$BO@odP3#bXW&&*~ zZ>Bfhdo&9Hhb%-cBeFde&1|GMayVf(DkpNql~k3Slrq_R2()1+7uRIHs@YLdTq7_Ib!V~ z8EUfz)z+feT5|gk^dX)AHvyM!>>+X&k@4`QbyO+1(F8 z01Z+$q#d+pBWi7jQrnSVgkS&-Q#NufdqCOfMZrqd!7C0vk2@biK?+0CXUollF}e?H z##={LwZX46_~pP11hZ(I!U+x1!S*T6K6zjof(V+VP|zTsx2m3D#WO62A49N&rYM}& zpeOHDJA+DRP!?7pc#0wv&JZX?$|{;?Y5Y(#lka!!a4m0xz>nq#d1Dkk`1kk=!tOyc delta 678 zcmXAn&2JJx7{+&Ds6b(tez2v>l8VGGOp_w5TM-e{7+Nibeu*YlQ+rUOhZ>8-Lv4tj z=&3YwD4y)agy@B)9(w6t;K7|_l8uKN8xLN`7*CqionewmX5Qa3@AJ%LKE{5Zl}04L zSFp#A_ghaY-8)i9^zQ3ty4)}H#o3sUC?0(3`r|zK>75asr9>Rf`97nBe>H!>J|fz) za^w^Q>J&xsSkR7zKI(w7<1292d+X)x<*nuBO9FK2w2LQpQEOCpH+D8!VFC>5g6NBX zV9-Xt+=9zvdLqs0yV~1YdzpYq>VfFguV{?=I1M+;R>Z&&!_3YTut5C~r6eW91OZ!6 z4>um}uI;R~G6YP}9*9;XMP*Q7Fw`)tzG2)q+*%>vDjk4mTT(@cK1eFJ(746Kq034y z;q;Pu_c;OUGy>6gF8eE~F4pTDI+cQsnC&bpL#zai&`}Ni{0&>)V=tO2Q|HK4ayXVV^Y;k2Pp?5#3}|+Tr-5*k$)-fEQ=559 zOJXhAwvDFfw8+>E^ev#&Zu0d^M>n(=w4xat&6tz73AjTG9L{zizjhYuS@ZfW0?M?= zVW|U??Vy#K#;Iwu^nid>I>+IB2d3JFHD18u1#|Hc0gvedhl>oNN>8tEiS6HXuaDY4 F>_2S}(^dcg diff --git a/tienda/__pycache__/vars.cpython-314.pyc b/tienda/__pycache__/vars.cpython-314.pyc index 3d2294311f3e6ef8d0f260b5be11139f790aa571..c41a8a37a16f36a566682775962a0b5f139efe8c 100644 GIT binary patch literal 2214 zcmZuzO-~y~7&ZidCJ++Q4@F9}NqdP(;;QYbiqhJSA(jkSYlE5`ieB#y_R!fKcXkKG zi;z(7{Sh2F(5lKU$Ly`A9HY5KJ@r(i{DD5RYiL!~2lkzr_w#w)cZScF7Hb9k4*&X< zH!Fq0A9iu^o0|#m??E^&e7_PG+_`wposZ|;EAbV#951*FanUWtCAaiR;eO@M6)2A0 zB`d$Izx(|gUB;>%O(^}1%>3tH>1{Zyy*_Db7EI&xgY!-+FV$FOXPLzzLC&kg%_4=VN1LEOPJw^sT)w9~B z^+Qkk?5I|2M>Z>H2&9cfQ5ESJCNWP?Voe(${XfWdEqVu>0tpPPj~PT6emwLoddSb> z7cqxC8j?+xha6xl>_yyG416@|*6ct!K943c;xRvv4{Pnv@hHM(?gQ0Ej2w+f{){AB zBpWe>$iYN`T|{L=?o+htygu@j= zDmd_2!`MWb&Kd_MVGi+{41MIDX|AUyzEq~VPC}6KWt1#-(ae`iQ&}g4mKf)z>*=W) zk;vG2s?$@CqhO1=U06tGu;+X({p;Yt7G{pJnCZ_pmcEM9P8h{J^z0OvQkRsRQgWk9 zdLPqa3B;@_%4_PTXV}e(>FOc+B}_i*Y7bKlnyj(_I0Jh{MOl_PhcqBPF@oCG={(FttrwmVv#lWM;aVMK`6bF#*!#`j*g`Dn!kR)V9=% zJ>j_NnKTPqmi2NE9V9?+rH9*=ge1KT-5$kEWqHrE%RT0eIT*5u_OzI!XE4zUb2r;` z)02@Drs#6HPsw#xqUX^5HVG7#eAq{$k<(H-nQi5!AHZLJw3gkJ7)L*SWudf@`YcRO zbHMr_&C<~4OevX7N?!&TUO;^1~XvEB-kRu*mEKdvXk$1s-TOZ@Wt@{*mChEAt_n8+* zR+vJNm%n8+t##RudyFvN+JFa$A{Fa|LxGiWlu6afmRKdcg2 z=l<}Ny`Lu2EzST(ch~q}&nVYh9AS)=dU}j`wyuriQ PzisrFQ6eRhg03h^vMIq32~n^qlD+_K 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 diff --git a/tienda/admin.py b/tienda/admin.py index 3f42731..685160b 100644 --- a/tienda/admin.py +++ b/tienda/admin.py @@ -1,12 +1,12 @@ from django.contrib import admin -from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage +from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, User, VerificationCode # Register your models here. admin.site.register(Category) admin.site.register(Image) admin.site.register(Product) - - +admin.site.register(User) +admin.site.register(VerificationCode) class CartItemInline(admin.TabularInline): model = CartItem extra = 0 diff --git a/tienda/migrations/0001_initial.py b/tienda/migrations/0001_initial.py index 428d4c3..71b71ea 100644 --- a/tienda/migrations/0001_initial.py +++ b/tienda/migrations/0001_initial.py @@ -1,6 +1,10 @@ -# Generated by Django 6.0.1 on 2026-01-23 09:33 +# Generated by Django 6.0.1 on 2026-03-10 07:56 +import django.contrib.auth.models +import django.contrib.auth.validators import django.db.models.deletion +import django.utils.timezone +from django.conf import settings from django.db import migrations, models @@ -9,6 +13,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -17,10 +22,160 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200)), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='tienda.category')), + ], + ), + migrations.CreateModel( + name='Image', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=200)), + ('image', models.ImageField(upload_to='images/')), + ('alt', models.CharField(blank=True, default='', max_length=255, verbose_name='Texto alternativo')), + ], + ), + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('registration_status', models.CharField(choices=[('CR', 'Confirmation Required'), ('AC', 'Active'), ('BN', 'Banned')], default='CR', max_length=2)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ - 'verbose_name_plural': 'Categories', + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_key', models.CharField(blank=True, max_length=40, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_key', models.CharField(blank=True, max_length=40, null=True)), + ('total', models.FloatField(default=0)), + ('status', models.CharField(choices=[('paid', 'Pagado'), ('cancelled', 'Cancelado')], default='paid', max_length=20)), + ('payment_method', models.CharField(choices=[('stripe', 'Stripe'), ('paypal', 'PayPal'), ('manual', 'Manual')], default='manual', max_length=20)), + ('payment_reference', models.CharField(blank=True, default='', max_length=200)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('buyer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_name', models.CharField(default='', max_length=200)), + ('quantity', models.PositiveIntegerField(default=1)), + ('unit_price', models.FloatField(default=0)), + ('total_price', models.FloatField(default=0)), + ('status', models.CharField(choices=[('pending', 'Pendiente'), ('processing', 'En preparación'), ('shipped', 'Enviado')], default='pending', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.order')), + ('seller', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items_to_fulfill', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('order_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='tienda.orderitem')), + ('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_messages', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['created_at'], + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=200)), + ('description', models.TextField(default='')), + ('briefdesc', models.TextField(default='')), + ('price', models.FloatField(default=0)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.category')), + ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_products', to=settings.AUTH_USER_MODEL)), + ('primary_image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.image')), + ('secondary_images', models.ManyToManyField(blank=True, related_name='products_secondary', to='tienda.image')), + ], + ), + migrations.AddField( + model_name='orderitem', + name='product', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.product'), + ), + migrations.CreateModel( + name='ShippingAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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(blank=True, max_length=250, 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(default='España', max_length=100, 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)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shipping_addresses', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Dirección de envío', + 'verbose_name_plural': 'Direcciones de envío', + 'ordering': ['-is_default', '-created_at'], + }, + ), + migrations.AddField( + model_name='order', + name='shipping_address', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='tienda.shippingaddress'), + ), + migrations.CreateModel( + name='VerificationCode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.TextField(default='')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_belongsto', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('added_at', models.DateTimeField(auto_now_add=True)), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.cart')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.product')), + ], + options={ + 'unique_together': {('cart', 'product')}, }, ), ] diff --git a/tienda/migrations/0002_alter_category_options_remove_category_parent.py b/tienda/migrations/0002_alter_category_options_remove_category_parent.py deleted file mode 100644 index a7abb93..0000000 --- a/tienda/migrations/0002_alter_category_options_remove_category_parent.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 6.0.1 on 2026-01-23 09:38 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='category', - options={}, - ), - migrations.RemoveField( - model_name='category', - name='parent', - ), - ] diff --git a/tienda/migrations/0002_verificationcode_code_mode_and_more.py b/tienda/migrations/0002_verificationcode_code_mode_and_more.py new file mode 100644 index 0000000..8dfb6c3 --- /dev/null +++ b/tienda/migrations/0002_verificationcode_code_mode_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.1 on 2026-03-10 11:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tienda', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='verificationcode', + name='code_mode', + field=models.CharField(choices=[('VA', 'Verify Account'), ('RP', 'Reset Password')], default='VA', max_length=2), + ), + migrations.AlterField( + model_name='verificationcode', + name='code', + field=models.TextField(default='', unique=True), + ), + ] diff --git a/tienda/migrations/0003_image.py b/tienda/migrations/0003_image.py deleted file mode 100644 index 93963c8..0000000 --- a/tienda/migrations/0003_image.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 6.0.1 on 2026-01-23 09:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0002_alter_category_options_remove_category_parent'), - ] - - operations = [ - migrations.CreateModel( - name='Image', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='', max_length=200)), - ('image', models.ImageField(upload_to='')), - ], - ), - ] diff --git a/tienda/migrations/0004_alter_image_image.py b/tienda/migrations/0004_alter_image_image.py deleted file mode 100644 index dbd9c16..0000000 --- a/tienda/migrations/0004_alter_image_image.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.1 on 2026-01-23 09:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0003_image'), - ] - - operations = [ - migrations.AlterField( - model_name='image', - name='image', - field=models.ImageField(upload_to='images/'), - ), - ] diff --git a/tienda/migrations/0005_product.py b/tienda/migrations/0005_product.py deleted file mode 100644 index be53e93..0000000 --- a/tienda/migrations/0005_product.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 6.0.1 on 2026-01-23 09:48 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0004_alter_image_image'), - ] - - operations = [ - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='', max_length=200)), - ('description', models.TextField(default='')), - ('price', models.FloatField(default=0)), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.category')), - ('primary_image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.image')), - ], - ), - ] diff --git a/tienda/migrations/0006_product_secondary_images.py b/tienda/migrations/0006_product_secondary_images.py deleted file mode 100644 index 062bf13..0000000 --- a/tienda/migrations/0006_product_secondary_images.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.1 on 2026-01-23 09:49 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0005_product'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='secondary_images', - field=models.ManyToManyField(blank=True, related_name='products_secondary', to='tienda.image'), - ), - ] diff --git a/tienda/migrations/0007_product_briefdesc.py b/tienda/migrations/0007_product_briefdesc.py deleted file mode 100644 index 8a8325d..0000000 --- a/tienda/migrations/0007_product_briefdesc.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-06 07:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0006_product_secondary_images'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='briefdesc', - field=models.TextField(default=''), - ), - ] diff --git a/tienda/migrations/0008_cart_cartitem.py b/tienda/migrations/0008_cart_cartitem.py deleted file mode 100644 index ec6272c..0000000 --- a/tienda/migrations/0008_cart_cartitem.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-06 10:41 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0007_product_briefdesc'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Cart', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('session_key', models.CharField(blank=True, max_length=40, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='CartItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity', models.PositiveIntegerField(default=1)), - ('added_at', models.DateTimeField(auto_now_add=True)), - ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.cart')), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.product')), - ], - options={ - 'unique_together': {('cart', 'product')}, - }, - ), - ] diff --git a/tienda/migrations/0009_product_creator.py b/tienda/migrations/0009_product_creator.py deleted file mode 100644 index c5cb169..0000000 --- a/tienda/migrations/0009_product_creator.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-06 10:48 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0008_cart_cartitem'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='creator', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_products', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/tienda/migrations/0010_order_orderitem.py b/tienda/migrations/0010_order_orderitem.py deleted file mode 100644 index 003b14e..0000000 --- a/tienda/migrations/0010_order_orderitem.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-09 09:06 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0009_product_creator'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Order', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('session_key', models.CharField(blank=True, max_length=40, null=True)), - ('total', models.FloatField(default=0)), - ('status', models.CharField(choices=[('paid', 'Pagado'), ('cancelled', 'Cancelado')], default='paid', max_length=20)), - ('payment_method', models.CharField(choices=[('stripe', 'Stripe'), ('paypal', 'PayPal'), ('manual', 'Manual')], default='manual', max_length=20)), - ('payment_reference', models.CharField(blank=True, default='', max_length=200)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('buyer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='OrderItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('product_name', models.CharField(default='', max_length=200)), - ('quantity', models.PositiveIntegerField(default=1)), - ('unit_price', models.FloatField(default=0)), - ('total_price', models.FloatField(default=0)), - ('status', models.CharField(choices=[('pending', 'Pendiente'), ('processing', 'En preparación'), ('shipped', 'Enviado')], default='pending', max_length=20)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.order')), - ('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.product')), - ('seller', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items_to_fulfill', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/tienda/migrations/0011_ordermessage.py b/tienda/migrations/0011_ordermessage.py deleted file mode 100644 index 17796b6..0000000 --- a/tienda/migrations/0011_ordermessage.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-09 09:12 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0010_order_orderitem'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='OrderMessage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message', models.TextField()), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('order_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='tienda.orderitem')), - ('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_messages', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['created_at'], - }, - ), - ] diff --git a/tienda/migrations/0012_image_alt_shippingaddress.py b/tienda/migrations/0012_image_alt_shippingaddress.py deleted file mode 100644 index d6e0f4e..0000000 --- a/tienda/migrations/0012_image_alt_shippingaddress.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-16 11:57 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tienda', '0011_ordermessage'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='image', - name='alt', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='Texto alternativo'), - ), - migrations.CreateModel( - name='ShippingAddress', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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(blank=True, max_length=250, 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(default='España', max_length=100, 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)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shipping_addresses', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Dirección de envío', - 'verbose_name_plural': 'Direcciones de envío', - 'ordering': ['-is_default', '-created_at'], - }, - ), - ] diff --git a/tienda/migrations/__pycache__/0001_initial.cpython-314.pyc b/tienda/migrations/__pycache__/0001_initial.cpython-314.pyc index 729551d20fd45cb8a197a946a69d67954596b206..851e7f431676d9c7f2be48b5710401e631313dd7 100644 GIT binary patch literal 12698 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 TspwzK%fiZ!!pd(1PMrN0B?lBX literal 0 HcmV?d00001 diff --git a/tienda/migrations/__pycache__/0003_image.cpython-314.pyc b/tienda/migrations/__pycache__/0003_image.cpython-314.pyc deleted file mode 100644 index 8954823a89ad15ecf7ab843e29d1a9e6418c82fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1074 zcmZuv&rcIU6rSC-+iqJ*`B5vxV3dYrA!!Mi5REY+N~jTn8&4)>GVM;w;&ykLof7Mb zgYnFff596^kDd(2+K`mY$;6YlMu=X0yGyADCfT>&n|bfeH{W{;nUN%d^?TJb>oP(= zeP^&@SK#6)fJ3x{bg7IsBra{pT)vBP=pNGLJklc-XOK`pN~%t9!2S8lEr9dK{w1Z;Vdtoa*Wd2;wh@OhV*j=>2cu&V-&?k;1)Yxfs#Y zgDha0(nok&&%ooeLfss^TExmt(x5^%jW_E;EoIv&L~X;KjltgmR?pmox+I7mz0V~g4izAIu-qCquDs4drWh=IAI z$t(j@g;F=EWq6;_79=a&q_#oC(&FOc5+)X>4A+20!vO-08}tb?+H?-+aNi}2+FTnE z2^^5YSR^rqX0|K}IF0dsi&#EE@J2DNo6O^uX;a(rj}7X=oz$+G)MNgbh}2r}_Dz2r znBO6her7gSL6vo%t5DZ=2@7E|knajRE*;EL4r7nQT-7k)oSD@Yryj;E2gNSnL*jL5 z*J;v{L2M{la+$M7Yuxie8-Q~Smab7L)TT6?*b=C;j00(lZkzdI9~d8a`(QqyzJk=U z)Q`xM%HgYi5~<#7s9ab3S(a$5`&@pqHQT=I6gePYa#5xMQ=kyw}O{^RfK#t=^Q@ zozhP3f1Nm+deOL=Jn||2IKI+G#lbOp{Tut^P>bUapC7T^^*n&Y) zjXh#FoT5<;bPc2oS^7GJu?0XP@i^?(6E+8~Kf&G|F#U)mNq-e2X_v~Vq#PxC*~0g1 K;ST~Q$o>b$Z5b2* diff --git a/tienda/migrations/__pycache__/0004_alter_image_image.cpython-314.pyc b/tienda/migrations/__pycache__/0004_alter_image_image.cpython-314.pyc deleted file mode 100644 index 85b1d13272f17e5bc6f2dcb917fb4c6872b15032..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 791 zcmZuv&u`N(6n@SRHwgtwVOkLkG?~)E(7j5Q!y~INIjV8F}FO|BQbcRts3q+JAxunB1 z;A67+ted{kNdRD2Di?Tj|vPyQlg30UA`pHx5?P zkj?f@;rJ8IRwm7881(svN-^c0OQby>#c2>Sk-9p1P`X(X47oIlXr4tp4!^-M zI;0}vNf=0{-EKc(1%-P>TC6zFm~%RwqAVA2l<*`~+aVtz z7f*T-&od=kIxk|uvsdc6%qSL)l-6QFI_Ze7o4dtWF0j!(76_3=r(?l$#xm8rC4L~g z-n(?jy)a0S%o}CtfcHeMzyv{h2_9Y|uXN^Zp3p;&`z%nBN~dLPjV77;CyJmz{shA( zSWvKc7rJ(fPTwpXuy4++opWnvzI|_gqcN{HX7%QIz4Gg{(>ne5I%}-C zj$K3A;UGx*X)8P}I#P6@yiIv4&)!BPt*2$5pJjJ2RnMQ_#jwx`A%7?k_kval{p`$Y Nd*5q&zX6k?`Y(v0#QXpN diff --git a/tienda/migrations/__pycache__/0005_product.cpython-314.pyc b/tienda/migrations/__pycache__/0005_product.cpython-314.pyc deleted file mode 100644 index 1bc7953dfd5ab40178158c065d29a580019e9624..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1652 zcmbtUO-vg{6y9Ci>mLl*#@I0}1SpE)pxRbRi!7y;C;{P?MAkwp1qnuL?_f48yUy%5 z3LM%Ts-AP?)?9nbk)GNkJ=L}>tu;L~QhUkG5>d{5v&#Zf=&56Q=6i3xdGqGYH#wL~{qvzo4ACd3B4SOpB zm3G23pkxXn4Q_|3ai4rSLN=lzZHKFgOP`N;rtk|YAw^ycdfo*|2?IOjb1p||YDI?10m6Ig?YiA|!Klh%!X78kuT0Gamf>e-|F+UTy$CQ$$W6C&aBg>)>d`+-$ zl~`JH%g!w--QhVGZx9ErWve)Mv=RW`pjrAr0GgqBpq^^CID@VIc@y$1GBZZ5kFiJQ}4s zHZ`^2J0MupG7)GNQ_Li4v0*$V-G@C6jk`Y-sk+ z?t_rMH7rvqmF^pyso_|5t{i&;(VzxZ*4YznWx zj#NsCUTaZoBSa=3nKSqcY@@hm9pIv-nvhblNvy}XPHhej0azE{lM7Vz>Q}t{t`wn? zpCVYog;Z!B5*Z@9T^NA<8oD2afhL({&C&ynrb+UK5 z**hn})4BE*YMZ2&wc82WPiBuZPA=cgHTye@ur>r>TtxkEX zjhem5C+!63C#PTDa%LC0vkNEb)65^a-*ZmoQK$0Asnj}^np4^DRQB7=?|QS}4+J5( zBn^hc>5;dgSTr^W#8dJQUy-7JTEY9^7X0fXw}Hl@7xS)8TxobUib>Fj)c3&F|y2kaCk&v zS3TcTQ-$7%{p6i+G8+4wh9vR(R46=!l%3CAM6^7vpF}hczoJ7>$S4{Kr`c@YC5w}Q zn@REtLWE5SbQs1IutUiEp&!dG;H?pI5@n3XQ9_ecrb9XaS~LkF$}%~=FrS3sb9r2* zNq6nQPew426uv`z@)a9aWKInScbkW)rTCPki0p+(quoAzlV^b_dV0J1$A za3;YBK)M9Cm#9&U*I2!S1`K+y9CW4VLhbrsl*#v%8wuDWFrU!OK-SJoRjwN2S2G(~ zS0?7xnYlIHxG}xFGj%s7?%tWZ_l2K6eSP{S`r-D@E1G4`RKqnszRa$b9S>`ou)?07 zbklm+$rZ|VDbAFG7JIJ)66S+K_lMawxbVjyCL7IEjPV}>;o7<3VB@nrsqTKS?*2yL HTcL|y{U+%T4-VwZdzpMM?|qpETUTua>)Yp#;W z026eIJp2M3V}Xw~q1{2-=pOR49pvd9+$O80&HYF(u+Tl@K5|OG6m>Ld55ivRi!g~Z zNd^hyQ5I~v5Hgo!(FZU=9VD=agmy|m$ZTL?bV}{YzYPTjGuYEQ<#zSIdtjmk0oC$K zt)(?kvpgbfuM7q)X}w-KljUKWuu&kSb(V&_%Xk(@fNplHGrA;YFjv@}YLg~Aa zA1PU2*C_3VX(pmD=5eC786Sc?kAskBsY)ZwXN)~p-%kxitvCF=$howWAzuxAo6=0c z@B``#k%ng@!81zJEofQ5g=EKln+&+i{1~)c@ZINu$P^d@NY>zRjoc!syZNmyWNxi9 zuLsx|j#G6`@}fZY2+BE{6J#CDb?qjZyqep{x-m8D7iN97b9Z)qcUHSKt?ggb_P-qd zs5LIjhGoxnQX?N;rngsGCypU4*7xIH(qL!#IPxJBUsKtNck6(p`M4PJlXMTLIuh9~ dl(~*E{zDLUE=dKGPxiEW@V$EQ8v)6?{{rFj&fx$6 diff --git a/tienda/migrations/__pycache__/0008_cart_cartitem.cpython-314.pyc b/tienda/migrations/__pycache__/0008_cart_cartitem.cpython-314.pyc deleted file mode 100644 index 8df31865c7fec25e08cce8457f265df9efe26c8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2476 zcmcguO-vg{6rNq%%i1_#69eJbke~)r1xP8vhqMaer?@dG*$^tyz-sXhW)pkY*&UO@ zp-rW>=NviqQmMTqr_xh%sg$0&b}cO$scNO3dUFM8uYI#?gR788m8!1f-S=kR&-dQE z_r|ZcoCzU#KKTAK?Gpi^-xyPW0yW}r8Hh({9Z6gY5e~k5ieK;%U!1M^QvQVi2`mUi zh@*Bih$Md}k^*airIxy(lkb~8k0wnLcL61Y-x+%&!BNq|glKx!O7Jw4*RrNUG()$j zkT+DEvoa?p5NfS4hj+l*BeVvXBt-aik@%$ent#cSs8Im{sx{$MJe(&t$`cVZ+y1G= zIEaUn__fed$EkZh$tU@xfFw-$z5NMZY69tVr%SI7gS&uS^MuZCbdf)QfEU2ockYrcS+s#vzXxFWO}55ac+#0 zdh2+g4`NauX_xw;I|pfSvEGT)Kckq0@|wClPx;hr!cW6Wkr;Bu#0tSGZ7!HvUNLv% z`*?>2Eo^E^PTR#ayp7Eb!@{zzFM`*E-;6+En5>g&5FEkuzywpC7%*A< zE(l>$!#UNWA`2-+aGr_}ii%D&vIEYVRJ^GabA*cE%=JS385Vm$15s%weFz zvJ8Rd7)an+WqDIGEt1o8tQ)dyvI$0G)zN&s4J_I^eS3L9zI|&> zk{92bolDd3v4n;#+C1Zy!Xg{kCc8Um_?niTh9a3~`KG~{EyeW6GqaGbWi5|AHZ*UT zSj+0yp>zcGo}y-s z_+rANbTy2EXC1M>EcPEnJAaL~Ki+maN6MWe`~K%c_8r;Q%}S?bN6106{|mtx7%vZu zKOxU~`{wP+z=|Dt{~+48x9!9y%JGS3u}Xa2j=a;bw7*=5U$Y}KV0*8_=^80_jXa6% z&px03G4(^rN#8A{?>cE*N@FLTFQxOgx%(GD=dSaAc%LJLy3>3Wq39xCZ5CoJM}c54 zRE;%&?s+BXp1l>PFInzOK8sfRX6(r9LDXHkT<*KPPbz)0c4V#*+{!nrPgkAExzgmE zGkK#ldBd4pDNU}}IA=fjsM7T@gEGw@M7#Iqo!(33-b?$!cfoIiPI94?TyT;%OUau~ zaiA<{0d07uo0 zQSZOUXuS%}Nmf41wf&5tZ+d+ZJl1eq$gO>^6j;%D#@}M9;uWtI-!`X#WtY3f?%Ar3 hr zHN0ZP9>Wm!AP*iw9-gxIoy~>qeh{`yLEd=^ard{EF|$#n&W#|Y>!#Rd1F*GNK5;OI zQy`WD1!GQO9*!7wMnK)15zhl>*RlO~8=tRm5(k!(l65KUZknb?SO`O_ph#Bnj_o1V zqLPRv&q^&>Meox{$d4Xa^8nZMz zOtbV5pQi`eXnJM*U{=9D*$iE{h6RUyyafgc5MA4`voWExY58gqEwn%bQc@u#KtiZdYm@?o$Ze1+l&+k3Qw)wB>H1E=1CTEUgEourl8l|1uic4v0>&G^R9Nfp7`{r16Xi3t4; zjsB8*U~d(`JG72;VF@t-w74V|B_>UVK4~dYl$l&qm@?_Gc7 zTb|Es%b|`-h?f9z_?Z9JXf};11eml5S5T+6>4op-rwZlOA}OzEI$3_Ys6FQC!3m4{ z92ZRUL0F&{^2XH%u1Bp6=MmlHiuuxTHe4>6RgR-4!@Tk2LSbd0u&8m>ZPNY>o+iX+ zFpo82Fy>j+7Nb7YHVP^C;6wa*{)OA1dDC#hP**{zYzJood~F9`+s47{e0z5O2QGAFX-s+dxSK|) z(Ev~E;EB%Jsm{oFCvz^yOz&i-w`V@DetGtN?dw{gmD^f5(AL}9dZ5+YTD^!lj>R>KB^bS56Ab RM>QCpX%El*MPNiZ{sYm?HCzAy diff --git a/tienda/migrations/__pycache__/0010_order_orderitem.cpython-314.pyc b/tienda/migrations/__pycache__/0010_order_orderitem.cpython-314.pyc deleted file mode 100644 index 6d6fc1b5193b932f8cf1260770853085b4117437..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3646 zcmcgvO>Eo96{aYOqGZRB64_4tCoAzfR-DLkoRzm|TEy{B?9`IHjFWVCA+fYLl9{DQ zg&a9m4s8xaf!-F_i;n?%X>TcdsE-Tu;+#UM0%#0Sw7?z~y}6rUd+i%iinSYik)mj2 zi}PmYdvD&)H#5|9->DD-pU;2$8zl$Qm$cLQ_}jv(O%R?k1xE0!G1vnwyT-11u{TD? zylcKyKlZP3n2Ry}Oq>yXgN)$M`#1VJhK}0nURTBe!E=#GaQ~p)Y{El$6Je}qWi!E& zP(>*l5>|B0BwR(8k!lu?OfbwqyYuP}c=(jbgHHm3*#eKf!qvQQ!-=R{fdQ}b+_C<6 zTwIdHJZPTzwifL{JRX6~hc-H^c0pjy3x7VrFL3idcVdne0)Rf(1#62GU6=wqKi|#* zV1s}iIt~k&4b8W`16UpUic z`z&Bbj`JC?(Jr6+zXvzag&PFi*a`N$`S8Ytx92vZ$Ba!JGdB75m~ifRE(qu6+l-%M zlR;r^(D1<$uw5d+-2uxI=oaVN42SThT( ziGPPR@5+P=u>P?X{x7S0`RyDD7ljGoy)L#+p0qZw^?$*xOTtvwS&;ZYuYUO*xE(s; zHk^9{!ZaQfuE1%0jRZ5Dlb85&hKvltD)QzEVb_)tKGG}Iur3x2Bw-|zo~of#B%>~V zis~e2B14f>s@Jr`1%I z__2;96)u8+j7m~X#e_4lgli`8RV76R)j)AuSBl6q=+-wP@F0Cvsa`=E7ApvE>oQyx zl2(&c66uT^sDuop6%k<)UL#BukF-PWNJI(4x^8fq{#cY`nS^RpnMUc(@lN0{2maUU zkQGj+$uSdt*CxF-NDzXAs|sOAfq_&<+erb7b-2`gj?9%5q{=1 z6V$j!AM}hQf%cM5`r5d5KjAxy$JKG*oQYHwq8@O}iOv!tFridaONy%g9xiZ}aIPs5 zO{G#l0PozGoD}N@ghj7U!fi-pNv7!t79}VPn3YLz(Glsmml`s$p;Qs!?m8_UfpS)% zte{=r0FtoAdB<4BsWw$IuF!$t22BR=0fABoTmZJ1keu3Sm|fP!s|Kn{hE!Dk{6`HC z0drfaR>6V5vbLkp<QPe>$i1QnrBG=Fp?6voONwD)Rnd^9i=shq zeiAhwOVz5h1qw2vRRP-74SLa&(+l@DSH*jGmjyACTUuTxy>Ac-GD**(v!gQfMaQ5w z5b3?8low#Htx&Q+ezqmU6+TU;Uy;_n}gRodEUN zs$pb8$k)KlSMX)XCo|i61cce zF6T)pmaNu|0Ic9Vco595;6na2bI38l=>z_Z&0lKrmww0FQ#Tt^H~+#f9QwUc?&>zUP?~(q`cI?5+&B4oizWoX7W6{!#)}U#H@j+zl8E22Dn&YYGc%QXC zyw@7PZ-pO#*6yS|G~FDUe!kEeN?YOigUI>av^_lC9Gba%&&%{60lUqoB66)SwZYiMt?6}x4H7rR}Y0R8(fKYH_m`gAbj5Xcll>QMWcHtK1I`R1$C(m)cs3O6d+k3J3>}ViC2!<=lNWn7 zV8>^h@!7o#`)6D6j1|rvM22_o+oK;eM?d%?(i&Z~!b@%EQ_WavFVc$r&c(DSuGx^hzVWVyAPB zbk0scXrv!lr3%UK_SC*`fA`)erK z+0#arZo!`Rd{Ng*?uiq8qv0G*&Z**VCc1H5;}57Rf5ScGer$XKUHWswq<6)k*W>ZL g;uuf-ugv)0n5oy?dmiqykR6_AgeSgXpySy1H|t|l1ONa4 diff --git a/tienda/migrations/__pycache__/0011_ordermessage.cpython-314.pyc b/tienda/migrations/__pycache__/0011_ordermessage.cpython-314.pyc deleted file mode 100644 index b0db5c8b58ebb8590fac4e5ca239cf664913112a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1869 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 B*oy!F diff --git a/tienda/models.py b/tienda/models.py index 7eddd51..e391fac 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -1,6 +1,31 @@ from django.db import models -from django.contrib.auth.models import User +from django.contrib.auth.models import User, AbstractUser from .vars import VAT_RATE +import random, string +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 + ) + +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.TextField(default = "", unique=True) + code_mode = models.CharField( + max_length=2, + choices = VerificationModes.choices, + default = VerificationModes.VERIFY_ACCOUNT + ) # Create your models here. class Category(models.Model): @@ -105,6 +130,7 @@ class Order(models.Model): ] 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) diff --git a/tienda/static/js/checkout.js b/tienda/static/js/checkout.js index bfed0d8..a5215d6 100644 --- a/tienda/static/js/checkout.js +++ b/tienda/static/js/checkout.js @@ -45,6 +45,14 @@ document.addEventListener("DOMContentLoaded", () => { console.log("Stripe initialized"); button.addEventListener("click", () => { + const shippingAddressSelect = document.getElementById("shipping-address"); + const selectedShippingAddress = shippingAddressSelect ? shippingAddressSelect.value : ""; + + if (!selectedShippingAddress) { + alert("Selecciona una dirección de envío para continuar."); + return; + } + console.log("Checkout button clicked"); button.disabled = true; button.innerHTML = "Procesando..."; @@ -55,7 +63,9 @@ document.addEventListener("DOMContentLoaded", () => { "Content-Type": "application/json", "X-CSRFToken": getCookie("csrftoken") }, - body: JSON.stringify({}) + body: JSON.stringify({ + shipping_address_id: selectedShippingAddress + }) }) .then((res) => { console.log("Session response status:", res.status); diff --git a/tienda/tasks.py b/tienda/tasks.py new file mode 100644 index 0000000..5a7de53 --- /dev/null +++ b/tienda/tasks.py @@ -0,0 +1,7 @@ +from celery import shared_task +from .utilities import send_email +from .vars import login_message + +@shared_task +def enviar_correo_bienvenida(email_usuario, nombre_usuario): + send_email(email_usuario, "Inicio de Sesión correcto", login_message.format(name = nombre_usuario)) \ No newline at end of file diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html index e6a162d..e3586db 100644 --- a/tienda/templates/tienda/base.html +++ b/tienda/templates/tienda/base.html @@ -1,4 +1,5 @@ {% load static %} +{% load cache %} {% load compress %} @@ -69,6 +70,7 @@ {% block head %}{% endblock %} + {% cache 500 sidebar request.user.username %} + {% endcache %}
@@ -131,30 +134,31 @@ {% block content %}{% endblock %} + {% cache 500 footer %} + {% endcache %}
- + {% cache 500 scripts %} + {% endcache %} \ No newline at end of file diff --git a/tienda/templates/tienda/checkout.html b/tienda/templates/tienda/checkout.html index fbd9da4..4272896 100644 --- a/tienda/templates/tienda/checkout.html +++ b/tienda/templates/tienda/checkout.html @@ -49,6 +49,30 @@ {% if cart_items %} +
+
+
1) Selecciona la dirección de envío
+ {% if addresses %} +
+ + +
+ {% else %} +
+ No tienes direcciones de envío creadas. + Crear dirección +
+ {% endif %} +
+
+
@@ -87,20 +111,22 @@
-

Selecciona tu método de pago

+

2) Selecciona tu método de pago

@@ -115,6 +141,13 @@ // Manejo del botón de PayPal document.getElementById('paypal-button').addEventListener('click', async function(e) { e.preventDefault(); + + const shippingAddressSelect = document.getElementById('shipping-address'); + const selectedShippingAddress = shippingAddressSelect ? shippingAddressSelect.value : ''; + if (!selectedShippingAddress) { + alert('Selecciona una dirección de envío para continuar.'); + return; + } const button = this; const originalText = button.innerHTML; @@ -138,7 +171,8 @@ headers: { 'X-CSRFToken': csrfToken || '', 'Content-Type': 'application/json', - } + }, + body: JSON.stringify({ shipping_address_id: selectedShippingAddress }) }); console.log('Response status:', response.status); diff --git a/tienda/templates/tienda/editar_direccion.html b/tienda/templates/tienda/editar_direccion.html index 46ddf30..d5bc6e3 100644 --- a/tienda/templates/tienda/editar_direccion.html +++ b/tienda/templates/tienda/editar_direccion.html @@ -35,17 +35,27 @@
- - + + + + {% for town in almeria_municipalities %} + + {% endfor %} + +
Selecciona o escribe un municipio de la provincia de Almería.
+
+ El pueblo/ciudad debe pertenecer a la provincia de Almería. +
- + +
Solo aceptamos códigos postales de Almería (04xxx).
- +
@@ -67,4 +77,64 @@
+ + {% endblock %} diff --git a/tienda/templates/tienda/mis_compras.html b/tienda/templates/tienda/mis_compras.html new file mode 100644 index 0000000..28dfdfd --- /dev/null +++ b/tienda/templates/tienda/mis_compras.html @@ -0,0 +1,63 @@ +{% extends "tienda/base.html" %} +{% load static %} + +{% block content %} +
+
+

Mis Compras

+ +
+
+ +
+
+ +
+
+ +
+
+

Total de compras: {{ total_orders }}

+ {% if orders %} +
+
+ + + + + + + + + + + {% for order in orders %} + + + + + + + + {% endfor %} + +
Pedido #FechaTotalEstadoMétodo
{{ order.id }}{{ order.created_at|date:"d/m/Y H:i" }}{{ order.total }}€{{ order.get_status_display }}{{ order.get_payment_method_display }}
+
+ {% else %} +
+ Aún no has realizado compras. +
+ {% endif %} + + +{% endblock %} diff --git a/tienda/templates/tienda/mis_recibos.html b/tienda/templates/tienda/mis_recibos.html new file mode 100644 index 0000000..49426d0 --- /dev/null +++ b/tienda/templates/tienda/mis_recibos.html @@ -0,0 +1,63 @@ +{% extends "tienda/base.html" %} +{% load static %} + +{% block content %} +
+
+

Mis Recibos

+ +
+
+ +
+
+ +
+
+ +
+
+

Total de recibos: {{ total_receipts }}

+ {% if receipts %} +
+ + + + + + + + + + + + {% for receipt in receipts %} + + + + + + + + {% endfor %} + +
Recibo #FechaTotalMétodoReferencia
{{ receipt.id }}{{ receipt.created_at|date:"d/m/Y H:i" }}{{ receipt.total }}€{{ receipt.get_payment_method_display }}{{ receipt.payment_reference|default:"-" }}
+
+ {% else %} +
+ No tienes recibos disponibles todavía. +
+ {% endif %} +
+
+{% endblock %} diff --git a/tienda/templates/tienda/pedidos_vendedor.html b/tienda/templates/tienda/pedidos_vendedor.html index 70ccbb3..833a9cd 100644 --- a/tienda/templates/tienda/pedidos_vendedor.html +++ b/tienda/templates/tienda/pedidos_vendedor.html @@ -43,6 +43,22 @@
  • Precio total: {{ item.total_price }}€
  • Fecha: {{ item.created_at|date:"d/m/Y H:i" }}
  • +
    Dirección de envío
    + {% if item.order.shipping_address %} +
      +
    • Destinatario: {{ item.order.shipping_address.full_name }}
    • +
    • Dirección: {{ item.order.shipping_address.address_line_1 }}
    • + {% if item.order.shipping_address.address_line_2 %} +
    • Detalle: {{ item.order.shipping_address.address_line_2 }}
    • + {% endif %} +
    • Ciudad: {{ item.order.shipping_address.city }}
    • +
    • Código Postal: {{ item.order.shipping_address.postal_code }}
    • +
    • País: {{ item.order.shipping_address.country }}
    • +
    • Teléfono: {{ item.order.shipping_address.phone }}
    • +
    + {% else %} +

    Dirección no disponible.

    + {% endif %}
    Cambiar Estado
    diff --git a/tienda/templates/tienda/portal_usuario.html b/tienda/templates/tienda/portal_usuario.html index 23b51ba..557497e 100644 --- a/tienda/templates/tienda/portal_usuario.html +++ b/tienda/templates/tienda/portal_usuario.html @@ -14,6 +14,8 @@
    Inicio + Compras + Recibos Mi Perfil Direcciones Mensajes @@ -29,6 +31,7 @@
    📦 Mis Pedidos

    {{ total_orders }}

    pedidos realizados

    + Ver compras
    @@ -54,6 +57,18 @@ +
    +
    +
    +
    +
    🧾 Recibos
    +

    consulta tus recibos de pago

    + Ver recibos +
    +
    +
    +
    +
    diff --git a/tienda/urls.py b/tienda/urls.py index d02b7d2..a8f3e3a 100644 --- a/tienda/urls.py +++ b/tienda/urls.py @@ -35,6 +35,8 @@ urlpatterns = [ path("paypal/execute/", views.paypal_execute, name="paypal_execute"), # Portal de usuario path("usuario/", views.portal_usuario, name="portal_usuario"), + path("usuario/compras/", views.mis_compras, name="mis_compras"), + path("usuario/recibos/", views.mis_recibos, name="mis_recibos"), path("usuario/perfil/", views.editar_perfil, name="editar_perfil"), path("usuario/perfil/cambiar-contrasena/", views.cambiar_contrasena, name="cambiar_contrasena"), path("usuario/direcciones/", views.direcciones_usuario, name="direcciones_usuario"), @@ -42,4 +44,5 @@ urlpatterns = [ path("usuario/direcciones//editar/", views.editar_direccion, name="editar_direccion"), path("usuario/direcciones//eliminar/", views.eliminar_direccion, name="eliminar_direccion"), path("usuario/mensajes/", views.mensajes_comprador, name="mensajes_comprador"), + path("verify/", views.verify, name="verify") ] diff --git a/tienda/utilities.py b/tienda/utilities.py new file mode 100644 index 0000000..8daa611 --- /dev/null +++ b/tienda/utilities.py @@ -0,0 +1,46 @@ +from django.core.mail import send_mail +import logging +from django.conf import settings + + +logger = logging.getLogger("email.system") +# +#def send_email(dest: str, title: str, body: str): +# context = ssl.create_default_context() +# try: +# with smtplib.SMTP(settings.SMTP_ENDPOINT, settings.SMTP_PORT) as server: +# +# +# server.ehlo() +# server.starttls(context=context) +# server.ehlo() +# server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD) +# +# message = """\ +#Subject: {} +#{} +# """.format(title, body) +# server.sendmail(settings.SMTP_EMAIL, dest, message) +# logger.info("EMAIL_SENT to=%s subject=%s", dest, title) +# +# except Exception as e: +# logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e)) +# return (False, e) +# +# return (True,) + +def send_email(dest: str, title: str, body: str): + try: + send_mail( + subject=title, + message=body, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[dest], + fail_silently=False, + ) + + logger.info("EMAIL_SENT to=%s subject=%s", dest, title) + return (True,) + except Exception as e: + logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e)) + return (False, e) \ No newline at end of file diff --git a/tienda/vars.py b/tienda/vars.py index 5750f32..3fc1273 100644 --- a/tienda/vars.py +++ b/tienda/vars.py @@ -1,2 +1,42 @@ PAGE_SIZE = 20 -VAT_RATE = 0.21 # IVA 21% \ No newline at end of file +VAT_RATE = 0.21 # IVA 21% + +# Restricciones de envío +SHIPPING_COUNTRY = "España" +ALMERIA_POSTAL_CODE_PREFIX = "04" +ALMERIA_MUNICIPALITIES_DISPLAY = ( + "Abla", "Abrucena", "Adra", "Albánchez", "Alboloduy", "Albox", "Alcolea", "Alcóntar", + "Alcudia de Monteagud", "Alhabia", "Alhama de Almería", "Alicún", "Almería", "Almócita", + "Alsodux", "Antas", "Arboleas", "Armuña de Almanzora", "Bacares", "Bayárcal", "Bayarque", + "Bédar", "Beires", "Benahadux", "Benitagla", "Benizalón", "Bentarique", "Berja", "Canjáyar", + "Cantoria", "Carboneras", "Castro de Filabres", "Chercos", "Chirivel", "Cóbdar", + "Cuevas del Almanzora", "Dalías", "Enix", "Félix", "Fines", "Fiñana", "Fondón", "Gádor", + "Gallardos", "Los Gallardos", "Garrucha", "Gérgal", "Huécija", "Huércal de Almería", + "Huércal-Overa", "Íllar", "Instinción", "Laroya", "Laujar de Andarax", "Líjar", "Lubrín", + "Lucainena de las Torres", "Lúcar", "Macael", "María", "Mojácar", "Mojonera", "La Mojonera", + "Nacimiento", "Níjar", "Ohanes", "Olula de Castro", "Olula del Río", "Oria", "Padules", + "Partaloa", "Paterna del Río", "Pechina", "Pulpí", "Purchena", "Rágol", "Rioja", + "Roquetas de Mar", "Santa Cruz de Marchena", "Santa Fe de Mondújar", "Senés", "Serón", "Sierro", + "Somontín", "Sorbas", "Suflí", "Tabernas", "Taberno", "Tahal", "Terque", "Tíjola", "Turre", + "Turrillas", "Uleila del Campo", "Urrácal", "Velefique", "Vélez-Blanco", "Vélez-Rubio", "Vera", + "Viator", "Vícar", "Zurgena" +) + +verify_message = """ +¡Buenas {name}! + +Muchas gracias por registrarte en Comercialmeria, para verificar que el correo que ha empleado es el suyo, por favor, haga click en el siguiente enlace. + +Si por alguna razón no es su correo, eliminelo inmediatamente y no le de click al enlace. + +{protocol}://{domain}/tienda/verify/{code} + +Este email ha sido automatizado +""" + +login_message = """ +¡Buenas {name}! + +Le enviamos este correo para indicarle que se acaba de iniciar sesión en un nuevo dispositivo. +En caso de que no sea usted, para proteger compras indebidas, ¡cambie la contraseña inmediatamente! +""" \ No newline at end of file diff --git a/tienda/views.py b/tienda/views.py index 1c560ea..ba257d4 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -1,11 +1,21 @@ from django.shortcuts import render, redirect, get_object_or_404 -from django.http import HttpRequest, JsonResponse +from django.http import HttpRequest, HttpResponse, JsonResponse from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout -from django.contrib.auth.models import User + from django.contrib.auth.decorators import login_required from django.contrib import messages -from .models import Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress -from .vars import PAGE_SIZE +from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, VerificationCode +from .utilities import send_email +from . import tasks +from .vars import ( + PAGE_SIZE, + VAT_RATE, + SHIPPING_COUNTRY, + ALMERIA_POSTAL_CODE_PREFIX, + ALMERIA_MUNICIPALITIES_DISPLAY, + verify_message, + login_message +) from django.conf import settings from django.views.decorators.csrf import csrf_exempt from django.urls import reverse @@ -13,8 +23,78 @@ from decimal import Decimal, ROUND_HALF_UP import stripe from django.db import models, transaction from django.core.cache import cache +import re +import unicodedata +import json +import random, string +import logging # Create your views here. + +logger = logging.getLogger("tienda") +audit_logger = logging.getLogger("tienda.audit") + + +def _normalize_location_text(value: str) -> str: + normalized = unicodedata.normalize("NFD", (value or "")) + without_accents = "".join(char for char in normalized if unicodedata.category(char) != "Mn") + without_symbols = re.sub(r"[^a-zA-Z0-9\s-]", "", without_accents) + collapsed = " ".join(without_symbols.replace("-", " ").lower().split()) + return collapsed + + +ALMERIA_MUNICIPALITIES = { + _normalize_location_text(municipality) + for municipality in ALMERIA_MUNICIPALITIES_DISPLAY +} +ALMERIA_MUNICIPALITIES.update( + { + municipality.removeprefix("la ") + for municipality in ALMERIA_MUNICIPALITIES + if municipality.startswith("la ") + } +) +ALMERIA_MUNICIPALITIES.update( + { + municipality.removeprefix("los ") + for municipality in ALMERIA_MUNICIPALITIES + if municipality.startswith("los ") + } +) + + +def _is_almeria_postal_code(postal_code: str) -> bool: + """Valida que el código postal pertenezca a la provincia de Almería (04xxx).""" + normalized = (postal_code or "").strip() + return len(normalized) == 5 and normalized.isdigit() and normalized.startswith(ALMERIA_POSTAL_CODE_PREFIX) + + +def _is_almeria_city(city: str) -> bool: + """Valida que el municipio/pueblo pertenezca a la provincia de Almería.""" + return _normalize_location_text(city) in ALMERIA_MUNICIPALITIES + + +def _address_form_context(direccion=None): + return { + "direccion": direccion, + "almeria_municipalities": ALMERIA_MUNICIPALITIES_DISPLAY, + } + + +def _get_client_ip(request: HttpRequest) -> str: + forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") + if forwarded_for: + return forwarded_for.split(",")[0].strip() + return request.META.get("REMOTE_ADDR", "") + + +def get_price_with_vat_decimal(price) -> Decimal: + """Retorna un precio con IVA aplicado y redondeado a 2 decimales.""" + return (Decimal(str(price)) * (Decimal("1") + Decimal(str(VAT_RATE)))).quantize( + Decimal("0.01"), + rounding=ROUND_HALF_UP, + ) + def home(request: HttpRequest): """Página de inicio del sitio""" categorias = Category.objects.all() @@ -50,32 +130,57 @@ def login(request: HttpRequest): email = request.POST.get("email") password = request.POST.get("password") remember = request.POST.get("remember") + client_ip = _get_client_ip(request) # Buscar usuario por email try: user_obj = User.objects.get(email=email) username = user_obj.username except User.DoesNotExist: + audit_logger.warning( + "LOGIN_FAILED email=%s reason=user_not_found ip=%s", + email, + client_ip, + ) messages.error(request, "Correo electrónico o contraseña incorrectos.") return render(request, "tienda/login.html") # Autenticar usuario user = authenticate(request, username=username, password=password) + user = User.objects.get(username=user.username) + if user.registration_status == "CR": + audit_logger.info( + "LOGIN_FAILED email=%s reason=not_verified", email + ) + messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico") + return render(request, "tienda/login.html") if user is not None: auth_login(request, user) # Configurar duración de sesión if not remember: - # Si no marca "Recordarme", la sesión expira al cerrar el navegador request.session.set_expiry(0) else: - # Si marca "Recordarme", la sesión dura 2 semanas request.session.set_expiry(1209600) # 14 días en segundos - + + audit_logger.info( + "LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s", + user.id, + user.email, + client_ip, + bool(remember), + ) + tasks.enviar_correo_bienvenida.delay(user.email, "{} {}".format(user.first_name, user.last_name)) + # result = send_email(user.email, "Inicio de sesión correcto", login_message.format(name = "{} {}".format(user.first_name, user.last_name))) messages.success(request, f"¡Bienvenido {user.first_name or user.username}!") return redirect("index") else: + audit_logger.warning( + "LOGIN_FAILED email=%s reason=invalid_credentials ip=%s", + email, + client_ip, + ) messages.error(request, "Correo electrónico o contraseña incorrectos.") return render(request, "tienda/login.html") @@ -83,22 +188,28 @@ def login(request: HttpRequest): def register(request: HttpRequest): + if request.user.is_authenticated: + return redirect("index") if request.method == "POST": name = request.POST.get("name") email = request.POST.get("email") password = request.POST.get("password") password_confirm = request.POST.get("password_confirm") + client_ip = _get_client_ip(request) # Validaciones if password != password_confirm: + audit_logger.warning("REGISTER_FAILED email=%s reason=password_mismatch ip=%s", email, client_ip) messages.error(request, "Las contraseñas no coinciden.") return render(request, "tienda/register.html") if len(password) < 8: + audit_logger.warning("REGISTER_FAILED email=%s reason=password_too_short ip=%s", email, client_ip) messages.error(request, "La contraseña debe tener al menos 8 caracteres.") return render(request, "tienda/register.html") if User.objects.filter(email=email).exists(): + audit_logger.warning("REGISTER_FAILED email=%s reason=email_exists ip=%s", email, client_ip) messages.error(request, "Ya existe un usuario con este correo electrónico.") return render(request, "tienda/register.html") @@ -119,19 +230,37 @@ def register(request: HttpRequest): password=password, first_name=name ) + + audit_logger.info( + "REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s", + user.id, + user.username, + user.email, + client_ip, + ) - # Iniciar sesión automáticamente - auth_login(request, user) - request.session.set_expiry(1209600) # 14 días + ver_code = ''.join(random.choices(string.digits, k=12)) + + codigo = VerificationCode.objects.create( + user = user, + code = ver_code, + code_mode = VerificationCode.VerificationModes.VERIFY_ACCOUNT + ) + message = verify_message.format(name = name, protocol = settings.PROTOCOL, domain = settings.DOMAIN, code = ver_code) + email_result = send_email(email, "Verificación de cuenta", message) - messages.success(request, f"¡Cuenta creada exitosamente! Bienvenido {name}.") + messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.") return redirect("index") return render(request, "tienda/register.html") def logout(request: HttpRequest): + user_id = request.user.id if request.user.is_authenticated else None + email = request.user.email if request.user.is_authenticated else None + client_ip = _get_client_ip(request) auth_logout(request) + audit_logger.info("LOGOUT user_id=%s email=%s ip=%s", user_id, email, client_ip) messages.success(request, "Has cerrado sesión exitosamente.") return redirect("index") @@ -178,25 +307,61 @@ def get_or_create_cart(request): return cart -def create_order_from_cart(request, payment_method, payment_reference=""): +def _get_selected_shipping_address(request: HttpRequest): + """Obtiene la dirección seleccionada desde JSON o form-data y valida pertenencia al usuario.""" + shipping_address_id = request.POST.get("shipping_address_id") + + if not shipping_address_id: + try: + payload = json.loads(request.body.decode("utf-8") or "{}") + shipping_address_id = payload.get("shipping_address_id") + except (json.JSONDecodeError, UnicodeDecodeError): + shipping_address_id = None + + if not shipping_address_id: + return None + + try: + shipping_address_id = int(shipping_address_id) + except (TypeError, ValueError): + return None + + return ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first() + + +def create_order_from_cart(request, payment_method, payment_reference="", shipping_address=None): """Crea un pedido a partir del carrito actual y lo asigna a vendedores.""" cart = get_or_create_cart(request) - cart_items = cart.items.select_related("product", "product__creator") + cart_items = list(cart.items.select_related("product", "product__creator")) - if not cart_items.exists(): + if not cart_items: return None + order_total = Decimal("0.00") + items_with_totals = [] + + for item in cart_items: + product = item.product + unit_price_with_vat = get_price_with_vat_decimal(product.price) + line_total_with_vat = (unit_price_with_vat * item.quantity).quantize( + Decimal("0.01"), + rounding=ROUND_HALF_UP, + ) + order_total += line_total_with_vat + items_with_totals.append((item, unit_price_with_vat, line_total_with_vat)) + with transaction.atomic(): order = Order.objects.create( buyer=request.user if request.user.is_authenticated else None, + shipping_address=shipping_address, session_key=None if request.user.is_authenticated else request.session.session_key, - total=cart.get_total(), + total=float(order_total), status=Order.STATUS_PAID, payment_method=payment_method, payment_reference=payment_reference or "", ) - for item in cart_items: + for item, unit_price_with_vat, line_total_with_vat in items_with_totals: product = item.product OrderItem.objects.create( order=order, @@ -204,8 +369,8 @@ def create_order_from_cart(request, payment_method, payment_reference=""): product_name=product.name, seller=product.creator, quantity=item.quantity, - unit_price=product.price, - total_price=product.price * item.quantity, + unit_price=float(unit_price_with_vat), + total_price=float(line_total_with_vat), ) cart.items.all().delete() @@ -322,7 +487,7 @@ def mis_productos(request: HttpRequest): def pedidos_vendedor(request: HttpRequest): """Muestra los pedidos asignados al vendedor autenticado""" pedidos = OrderItem.objects.filter(seller=request.user).select_related( - 'order', 'product', 'order__buyer' + 'order', 'product', 'order__buyer', 'order__shipping_address' ).prefetch_related('messages__sender').order_by('-created_at') return render(request, "tienda/pedidos_vendedor.html", { @@ -540,9 +705,11 @@ def borrar_producto(request: HttpRequest, id: int): def checkout(request: HttpRequest): cart = get_or_create_cart(request) cart_items = cart.items.select_related("product") + addresses = ShippingAddress.objects.filter(user=request.user) return render(request, "tienda/checkout.html", { "cart": cart, - "cart_items": cart_items + "cart_items": cart_items, + "addresses": addresses, }) @csrf_exempt @@ -561,6 +728,10 @@ def create_checkout_session(request: HttpRequest): return JsonResponse({"error": "Método no permitido"}, status=405) try: + shipping_address = _get_selected_shipping_address(request) + if shipping_address is None: + return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400) + cart = get_or_create_cart(request) cart_items = cart.items.select_related("product") @@ -571,7 +742,8 @@ def create_checkout_session(request: HttpRequest): line_items = [] for item in cart_items: - unit_amount = int((Decimal(str(item.product.price)) * 100).quantize(0, rounding=ROUND_HALF_UP)) + unit_price_with_vat = get_price_with_vat_decimal(item.product.price) + unit_amount = int((unit_price_with_vat * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP)) if unit_amount <= 0: continue line_items.append({ @@ -601,18 +773,23 @@ def create_checkout_session(request: HttpRequest): ) request.session['stripe_session_id'] = session.id + request.session['selected_shipping_address_id'] = shipping_address.id return JsonResponse({"sessionId": session.id}) except Exception as e: - print(f"Stripe error: {str(e)}") + logger.exception("STRIPE_CHECKOUT_SESSION_ERROR user_id=%s error=%s", request.user.id, str(e)) return JsonResponse({"error": f"Error al crear sesión de pago: {str(e)}"}, status=500) def checkout_success(request: HttpRequest): payment_reference = request.session.get('stripe_session_id', "") - create_order_from_cart(request, Order.PAYMENT_STRIPE, payment_reference) + shipping_address_id = request.session.get('selected_shipping_address_id') + shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first() + create_order_from_cart(request, Order.PAYMENT_STRIPE, payment_reference, shipping_address) if 'stripe_session_id' in request.session: del request.session['stripe_session_id'] + if 'selected_shipping_address_id' in request.session: + del request.session['selected_shipping_address_id'] messages.success(request, "Pago realizado correctamente. ¡Gracias por tu compra!") return render(request, "tienda/checkout_success.html", {}) @@ -652,6 +829,10 @@ def create_paypal_payment(request: HttpRequest): return JsonResponse({"error": "Método no permitido"}, status=405) try: + shipping_address = _get_selected_shipping_address(request) + if shipping_address is None: + return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400) + import paypalrestsdk cart = get_or_create_cart(request) @@ -669,16 +850,24 @@ def create_paypal_payment(request: HttpRequest): # Crear lista de items para PayPal payment_items = [] + payment_total = Decimal("0.00") for item in cart_items: + unit_price_with_vat = get_price_with_vat_decimal(item.product.price) + line_total_with_vat = (unit_price_with_vat * item.quantity).quantize( + Decimal("0.01"), + rounding=ROUND_HALF_UP, + ) + payment_total += line_total_with_vat + payment_items.append({ "name": item.product.name, "sku": f"product_{item.product.id}", - "price": str(round(float(item.product.price), 2)), + "price": format(unit_price_with_vat, ".2f"), "currency": "EUR", "quantity": item.quantity }) - total = str(round(float(cart.get_total()), 2)) + total = format(payment_total, ".2f") # Crear el pago payment = paypalrestsdk.Payment({ @@ -713,6 +902,7 @@ def create_paypal_payment(request: HttpRequest): if payment.create(): # Guardar el payment ID en sesión request.session['paypal_payment_id'] = payment.id + request.session['selected_shipping_address_id'] = shipping_address.id # Encontrar el link de aprobación for link in payment.links: @@ -723,16 +913,15 @@ def create_paypal_payment(request: HttpRequest): else: # Loguear el error error_msg = str(payment.error) if hasattr(payment, 'error') else "Error desconocido" - print(f"PayPal Error: {error_msg}") + logger.error("PAYPAL_CREATE_ERROR user_id=%s error=%s", request.user.id, error_msg) return JsonResponse({"error": f"Error al crear el pago: {error_msg}"}, status=400) except ImportError: + logger.error("PAYPAL_SDK_NOT_INSTALLED") return JsonResponse({"error": "SDK de PayPal no instalado"}, status=500) except Exception as e: error_msg = str(e) - print(f"PayPal Exception: {error_msg}") - import traceback - traceback.print_exc() + logger.exception("PAYPAL_CREATE_EXCEPTION user_id=%s error=%s", request.user.id, error_msg) return JsonResponse({"error": f"Error: {error_msg}"}, status=500) @@ -766,11 +955,15 @@ def paypal_execute(request: HttpRequest): # Ejecutar el pago if payment.execute({"payer_id": payer_id}): # Pago exitoso - crear pedido y limpiar el carrito - create_order_from_cart(request, Order.PAYMENT_PAYPAL, payment_id) + shipping_address_id = request.session.get('selected_shipping_address_id') + shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first() + create_order_from_cart(request, Order.PAYMENT_PAYPAL, payment_id, shipping_address) # Limpiar la sesión if 'paypal_payment_id' in request.session: del request.session['paypal_payment_id'] + if 'selected_shipping_address_id' in request.session: + del request.session['selected_shipping_address_id'] messages.success(request, "¡Pago realizado correctamente con PayPal! Gracias por tu compra.") return render(request, "tienda/checkout_success.html", {}) @@ -780,6 +973,7 @@ def paypal_execute(request: HttpRequest): return redirect("checkout") except Exception as e: + logger.exception("PAYPAL_EXECUTE_EXCEPTION user_id=%s error=%s", request.user.id, str(e)) messages.error(request, f"Error: {str(e)}") return redirect("checkout") def search_suggestions(request: HttpRequest): @@ -829,6 +1023,31 @@ def portal_usuario(request: HttpRequest): }) +@login_required +def mis_compras(request: HttpRequest): + """Lista completa de compras del usuario autenticado""" + orders = Order.objects.filter(buyer=request.user).prefetch_related('items').order_by('-created_at') + + return render(request, "tienda/mis_compras.html", { + "orders": orders, + "total_orders": orders.count(), + }) + + +@login_required +def mis_recibos(request: HttpRequest): + """Lista de recibos (pedidos pagados) del usuario autenticado""" + receipts = Order.objects.filter( + buyer=request.user, + status=Order.STATUS_PAID + ).prefetch_related('items').order_by('-created_at') + + return render(request, "tienda/mis_recibos.html", { + "receipts": receipts, + "total_receipts": receipts.count(), + }) + + @login_required def editar_perfil(request: HttpRequest): """Edita la información del perfil del usuario""" @@ -908,14 +1127,22 @@ def crear_direccion(request: HttpRequest): address_line_2 = request.POST.get("address_line_2", "").strip() city = request.POST.get("city", "").strip() postal_code = request.POST.get("postal_code", "").strip() - country = request.POST.get("country", "España").strip() + country = SHIPPING_COUNTRY phone = request.POST.get("phone", "").strip() is_default = request.POST.get("is_default") == "on" # Validaciones if not all([full_name, address_line_1, city, postal_code, phone]): messages.error(request, "Por favor completa todos los campos obligatorios.") - return render(request, "tienda/editar_direccion.html") + return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST)) + + if not _is_almeria_city(city): + messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.") + return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST)) + + if not _is_almeria_postal_code(postal_code): + messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).") + return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST)) # Crear dirección ShippingAddress.objects.create( @@ -933,7 +1160,7 @@ def crear_direccion(request: HttpRequest): messages.success(request, "Dirección creada correctamente.") return redirect("direcciones_usuario") - return render(request, "tienda/editar_direccion.html", {"direccion": None}) + return render(request, "tienda/editar_direccion.html", _address_form_context()) @login_required @@ -947,7 +1174,7 @@ def editar_direccion(request: HttpRequest, id: int): direccion.address_line_2 = request.POST.get("address_line_2", "").strip() direccion.city = request.POST.get("city", "").strip() direccion.postal_code = request.POST.get("postal_code", "").strip() - direccion.country = request.POST.get("country", "España").strip() + direccion.country = SHIPPING_COUNTRY direccion.phone = request.POST.get("phone", "").strip() direccion.is_default = request.POST.get("is_default") == "on" @@ -955,13 +1182,21 @@ def editar_direccion(request: HttpRequest, id: int): if not all([direccion.full_name, direccion.address_line_1, direccion.city, direccion.postal_code, direccion.phone]): messages.error(request, "Por favor completa todos los campos obligatorios.") - return render(request, "tienda/editar_direccion.html", {"direccion": direccion}) + return render(request, "tienda/editar_direccion.html", _address_form_context(direccion)) + + if not _is_almeria_city(direccion.city): + messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.") + return render(request, "tienda/editar_direccion.html", _address_form_context(direccion)) + + if not _is_almeria_postal_code(direccion.postal_code): + messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).") + return render(request, "tienda/editar_direccion.html", _address_form_context(direccion)) direccion.save() messages.success(request, "Dirección actualizada correctamente.") return redirect("direcciones_usuario") - return render(request, "tienda/editar_direccion.html", {"direccion": direccion}) + return render(request, "tienda/editar_direccion.html", _address_form_context(direccion)) @login_required @@ -990,3 +1225,43 @@ def mensajes_comprador(request: HttpRequest): return render(request, "tienda/mensajes_comprador.html", { "order_items": order_items }) + + + +def send_test_email(request: HttpRequest): + message = """ + +Correo de prueba, deberias recibir esto bien +y esto deberia tener un enter + """ + + result = send_email("danilacasito8@gmail.com", "Correo de Prueba", message) + if result[0]: + return HttpResponse("Mira tu bandeja") + else: + return HttpResponse(result[1]) + + +def verify(request: HttpRequest, code: str): + obj = None + try: + obj = VerificationCode.objects.get(code=code) + except VerificationCode.DoesNotExist: + return HttpResponse("

    Error

    No existe el codigo de verificación

    ") + if obj: + if obj.code_mode == VerificationCode.VerificationModes.VERIFY_ACCOUNT: + + obj.user.registration_status = obj.user.RegisterStatus.ACTIVE + obj.user.save() + obj.delete() + return redirect("index") + else: + return HttpResponse("

    Error

    No existe el codigo de verificación

    ") + + +def reset_password(request: HttpRequest): + if request.user.is_authenticated: + return redirect("index") + + + return render(request, "tienda/reset_password", {}) \ No newline at end of file