From 473f13fb084317fc29344b0016955f22ae662e2b Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 2 Feb 2026 17:09:26 +0000 Subject: [PATCH] making all screens fit --- core/__pycache__/admin.cpython-311.pyc | Bin 5695 -> 6885 bytes core/__pycache__/models.cpython-311.pyc | Bin 26934 -> 33137 bytes core/__pycache__/urls.cpython-311.pyc | Bin 8112 -> 8735 bytes core/__pycache__/views.cpython-311.pyc | Bin 77867 -> 85937 bytes core/admin.py | 21 +- ...tytier_customer_loyalty_points_and_more.py | 78 +++++++ core/migrations/0015_userprofile.py | 26 +++ ...er_loyalty_points_and_more.cpython-311.pyc | Bin 0 -> 4476 bytes .../0015_userprofile.cpython-311.pyc | Bin 0 -> 1731 bytes core/models.py | 67 +++++- core/templates/base.html | 50 ++++- core/templates/core/pos.html | 197 +++++++++++++++--- core/templates/core/profile.html | 192 +++++++++++++++++ core/templates/core/settings.html | 191 ++++++++++++++++- core/urls.py | 7 + core/views.py | 170 ++++++++++++++- static/css/custom.css | 115 +++++++++- 17 files changed, 1059 insertions(+), 55 deletions(-) create mode 100644 core/migrations/0014_loyaltytier_customer_loyalty_points_and_more.py create mode 100644 core/migrations/0015_userprofile.py create mode 100644 core/migrations/__pycache__/0014_loyaltytier_customer_loyalty_points_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0015_userprofile.cpython-311.pyc create mode 100644 core/templates/core/profile.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 13f71e71756cac95e9af17987d17752e083037af..255213de67ac2e00a8e7a935707b7cdddec80ee1 100644 GIT binary patch literal 6885 zcmd^DOK%(36`nhMiXthRdRn$6*|KEQs;xK=J8g{?fvviRZQ6fl z*3wo+%UD@0YfWeqR!+-VliH+}*YZ|DD_BLXXiaHTR!J*aC$tk*Su0!9+O##J%~-SA ztTm_2S@YVwwV*99MO6&;<;g%1Bk`Om%J=l~UaE3iX(Yc@8Yz-m3`Up6XW zAKWO~sR&~-hA~ZynXws9M;KEvj9Frw7@P4-gfSh%m?Or?u^E>ljF}k5JTVr=W?YUi z&c-kniE(Od#uXXO60JEwYt9BC8;+)SPD+(YYWgiws}b$8G1|=#$L!eJosTe1#xTwi zZi$cWI*IxGl@zBD>vbx0@ziCAamaLHC4rT*DHR zxO%AXS%ysu9(H*BvF;cmA6!YHso*XUocOfUa&^~i*`g3$O3@tnJ~P}7w?(mkE$4~> zjXz%=zLzqE?LCL|+BRI*w0A|p|Hb=;`?%E***_Z123W=9z1E)IboV}oQRU!<>$aoU zp;tYqDELW{R^Eu z=rlXKe3_UkjGv*eDy_@bYErN!a1RKa+2oj$3P(PP?h^iRm^s z+O2k{Nx^H>zA(u!=Y{eLn*Mxw^D%{Ov$NB&-Oi@*jZyEoEx!46i$8JNdfnKpx45w> zli*5wkDsPB5(;$TtPpg=_m9(8yZ4Sxo&DF1gT?m_lJnjlUk{cH`6)PRUzKS(>L>C~ zLQEjTeBVgp`4WkWob$NFUFm}AizHtGJ8C*V2UrEH0p!nKz*Qvx{FF0IFsSqO?mfIc zd5FfV85d3pWkO5&;mrG6!P7W1pmD-^(h;dPH|vHd(k`h#L8xrkboWHI9&V@(nOjNl ztI+m+0x@AaHDuURgN6}(=>UF(uBtE6ah8io8TT*Drc0?>a15RHm>=+{S1yAPFh?H; z^n+fhQ6$SBbnhO`Tnvo>fT zMB@#7W1bd*f*0f}6cxpK%XTSxwYI^XmhCCdH)txV;_opAKo-A>rsuW6^n}zqYF8er zYJrOVZ2EvGtFe0Bq!|v7F8u^|a!+9>UWwrxFcK7hM*BmR+gVm<)ZIIF)3gnqB${ev zB#kF&Sfr?|8BI!zZ(OcN{j~*60NAXNkRIh{x?4y2LU#*qPhW`s0)0;moK|mFnVxDC z{s+KqfOJ>C`rN@70DEUd-T4u9Av&aPc%q>Ejczd`Wu$jiE#q&s38?MYAfcKKQ8ewR zEh;@VyJPL(e69y4b>5|ewOP}x7S(<++1AZQaFcfE$n7{HSLcR2G<5fc%0DFq_-6#w zN;Y-}egpv-A6OwWrVO>(gDlnU>khX$%I;%kqzJQDdoVI|-? zeIk+naax*rt$P=5&o+qu0^5pVSL7L&5~Z(#mdOt~uR;}RUuYWNrm-sj3uYpUYZwFE zpwEcnfA!LCAu}}I7q#twuPW|4qPU8TR#Wi?6O_x1;ExE(d*`*UXXf6 zy-GpvP7Q^pTB_eyi5zv|LGMwLJxI;!G+onsn3<+#x{gAL1HDd9F#4_$?^_c#0I=0Y zY}gsJ0aEccEYi$qtsiJN=L1{%&mn=g;b>8!lQVcgskwV?L!`aFN)~Wxr&=WCT{CJ{ z{4>FOw5akAF&U`}HpT#t=rhtV-bGdT5Dnvn!F~$z)yid2Y<#KPyR9qeinzJM6ErT; z@|bscif(uskO5=?69D|@kfS5u@{BFustA|@lmI6HC^HuaTj(_!W zcvWP%u}e=!bQI?9qs@?R^B!y}5W+dl(Fe+=Om;w`@VUN2k5Alh1muRk0}*+Y$wH4J zC~J}2@^W*@OqQu2{T6ZP&)FN^)7$U(*R+<=$N7O=ahza`9VsgZ!+*!>1XGWd{~wrB z)KYi$+ok>NAv@V)Cy!N?T|HJtVSlSPbN(eJUIShx6m>C_&ju{XLpIxEv&Sl9A0I2D zu%GIcR$gM_H2^Ggp?o@E$sDqU9$Pq86YTwCWfb;5>P@e{#Kda=SV(tYJ{PbIShNm{ zq7YiN4vW?YEm}VgEecr}yAF$@;Iq&KEV|xjfoNz^;gBu$*b@8_C6J`gW_u?q@JpX% zIh0SUtOx_5u>Ytxdl4Zi!T^6Z8(#ItaP=(JTi6&84ty2HZPeK^0DjyCJ?_N~@^Ky~ zD^-{}1fj>tN}A~V3R0oRCk`29)*>=-0|Zfk*=w+511uY0ITgx>EV)BQqo?5U2Mj)r z!+oPSw+^>IV2IuW2C~!rWnuh^hm7K}{C2iyYrVxwQMQ%-vXE^c8@8lwaTun=meg%a z>J}sgEeP2LGW3>Yi^DKYZ%MXWlFi$|jM0LSZSs(j15U%zx@3#P{Z4QG0xYddwz_0H q(_a>_9hA-;veh12#a6zEOp3xYqqlSgTlpfk@HY_)u$JZk delta 1942 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%0AlcG#_s2?~18An=JlPo^jH*o=uJ;*$8< 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 delta 1443 zcmYk6TTdHD6o9dNT-G+Bq+De0`UVLBN`cTMr3Hr=!Zr8?4z}YKdfMx+bjH-Y&YD#ccuS6R^9IUW*N62aqIQ1GS=}g zx2xWeu?L%Uz1}Nh9}epJnqS5Nk^E?a*gMPntm`tFND@d3>m6m}Tw@Bt~DuL)r3H~ zlXV2i)f)sd6dE!NGW>!xKEbcO?%7XeJp8sjOvV-oBq<~rB>9LGf;FL|Jju?AbZ@=y zvJ^(~nW5KTk#PuL8iqDzkuZh-8Afzk!HlJr$5*zoz$Frl5QtKUGKlh7rCc)=`Dx>1 zc9}qqLXJU>&nYF0F`uDD6fEFFkD(euL`l~P6e$!L z6!{IMWK2cAd4|M_1WFW23`+bnrQ~qfYiKtWtl*M>ynj3f9d``tc<5TqGkg z0&xm)260|gO374uDwkOABnht&$WzEO$ny=cvY&f9)SBKyr%%`R)l&!9C*Y2PyC-F^ zA1b(K!GC?a{l21*6&rkR`It+k0eQ(k19Lr*seL?}cUMEJZi7ED8Kpb;{@Kp;UO!63mCO1W+-a`r`8=HBd~ zMWrN7#dEyMd>05ddK$Ep>iZo36r)>Gkj18e-n*+eq7Y&b;sqtFn?iS`^M`@w&Nt&MDxrkW0=l-LU=v>o*ix{E zuLFj*uiyaN+l|y61$WJ)7ptr6<9U+aBCt(io540eRLVV5ksE)CM7|`jMq!P?8ow`< NBj?Gv8y7#%{{f%gyA=Qc diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 97afd4a3b554c8a17c83b0887bd16736cf5ec1a8..0c44493d52af748b74093d63192f5898d175aee9 100644 GIT binary patch delta 22430 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 delta 17110 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 diff --git a/core/admin.py b/core/admin.py index 49646aa..130daf1 100644 --- a/core/admin.py +++ b/core/admin.py @@ -6,7 +6,8 @@ from .models import ( Quotation, QuotationItem, SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, - SystemSetting, PaymentMethod + SystemSetting, PaymentMethod, HeldSale, + LoyaltyTier, LoyaltyTransaction ) @admin.register(Category) @@ -26,7 +27,7 @@ class ProductAdmin(admin.ModelAdmin): @admin.register(Customer) class CustomerAdmin(admin.ModelAdmin): - list_display = ('name', 'phone', 'email') + list_display = ('name', 'phone', 'email', 'loyalty_points', 'loyalty_tier') search_fields = ('name', 'phone') @admin.register(Supplier) @@ -71,4 +72,18 @@ class PurchaseReturnAdmin(admin.ModelAdmin): @admin.register(SystemSetting) class SystemSettingAdmin(admin.ModelAdmin): - list_display = ('business_name', 'phone', 'email', 'vat_number') \ No newline at end of file + list_display = ('business_name', 'phone', 'email', 'vat_number') + +@admin.register(HeldSale) +class HeldSaleAdmin(admin.ModelAdmin): + list_display = ('id', 'customer', 'total_amount', 'created_at') + +@admin.register(LoyaltyTier) +class LoyaltyTierAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'min_points', 'point_multiplier', 'discount_percentage') + +@admin.register(LoyaltyTransaction) +class LoyaltyTransactionAdmin(admin.ModelAdmin): + list_display = ('customer', 'transaction_type', 'points', 'created_at') + list_filter = ('transaction_type', 'created_at') + search_fields = ('customer__name',) diff --git a/core/migrations/0014_loyaltytier_customer_loyalty_points_and_more.py b/core/migrations/0014_loyaltytier_customer_loyalty_points_and_more.py new file mode 100644 index 0000000..2842852 --- /dev/null +++ b/core/migrations/0014_loyaltytier_customer_loyalty_points_and_more.py @@ -0,0 +1,78 @@ +# Generated by Django 5.2.7 on 2026-02-02 16:46 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0013_heldsale'), + ] + + operations = [ + migrations.CreateModel( + name='LoyaltyTier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(max_length=50, verbose_name='Name (English)')), + ('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')), + ('min_points', models.PositiveIntegerField(default=0, verbose_name='Minimum Points')), + ('point_multiplier', models.DecimalField(decimal_places=2, default=1.0, max_digits=4, verbose_name='Point Multiplier')), + ('discount_percentage', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Discount Percentage')), + ('color_code', models.CharField(default='#6c757d', max_length=20, verbose_name='Color Code')), + ], + ), + migrations.AddField( + model_name='customer', + name='loyalty_points', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Loyalty Points'), + ), + migrations.AddField( + model_name='sale', + name='loyalty_discount_amount', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Loyalty Discount'), + ), + migrations.AddField( + model_name='sale', + name='loyalty_points_redeemed', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Loyalty Points Redeemed'), + ), + migrations.AddField( + model_name='systemsetting', + name='currency_per_point', + field=models.DecimalField(decimal_places=3, default=0.01, max_digits=10, verbose_name='Currency Value per Point'), + ), + migrations.AddField( + model_name='systemsetting', + name='loyalty_enabled', + field=models.BooleanField(default=False, verbose_name='Enable Loyalty System'), + ), + migrations.AddField( + model_name='systemsetting', + name='min_points_to_redeem', + field=models.PositiveIntegerField(default=100, verbose_name='Minimum Points to Redeem'), + ), + migrations.AddField( + model_name='systemsetting', + name='points_per_currency', + field=models.DecimalField(decimal_places=2, default=1.0, max_digits=10, verbose_name='Points Earned per Currency Unit'), + ), + migrations.AddField( + model_name='customer', + name='loyalty_tier', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customers', to='core.loyaltytier'), + ), + migrations.CreateModel( + name='LoyaltyTransaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('transaction_type', models.CharField(choices=[('earned', 'Earned'), ('redeemed', 'Redeemed'), ('adjusted', 'Adjusted')], max_length=20, verbose_name='Type')), + ('points', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Points')), + ('notes', models.TextField(blank=True, verbose_name='Notes')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loyalty_transactions', to='core.customer')), + ('sale', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loyalty_transactions', to='core.sale')), + ], + ), + ] diff --git a/core/migrations/0015_userprofile.py b/core/migrations/0015_userprofile.py new file mode 100644 index 0000000..f62b301 --- /dev/null +++ b/core/migrations/0015_userprofile.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-02-02 16:58 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0014_loyaltytier_customer_loyalty_points_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(blank=True, null=True, upload_to='profile_pics/', verbose_name='Profile Picture')), + ('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone Number')), + ('bio', models.TextField(blank=True, verbose_name='Bio')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/__pycache__/0014_loyaltytier_customer_loyalty_points_and_more.cpython-311.pyc b/core/migrations/__pycache__/0014_loyaltytier_customer_loyalty_points_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43af3a24501dddd17cae1b6507a4d7a3ad16693b GIT binary patch literal 4476 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 ZZ&w)+lrDIGcpmkV6PH|Z{Q<4-)xY`r+sOa` literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 71a240b..6d25482 100644 --- a/core/models.py +++ b/core/models.py @@ -2,6 +2,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.utils import timezone from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver class Category(models.Model): name_en = models.CharField(_("Name (English)"), max_length=100) @@ -42,15 +44,50 @@ class Product(models.Model): def __str__(self): return f"{self.name_en} ({self.sku})" +class LoyaltyTier(models.Model): + name_en = models.CharField(_("Name (English)"), max_length=50) + name_ar = models.CharField(_("Name (Arabic)"), max_length=50) + min_points = models.PositiveIntegerField(_("Minimum Points"), default=0) + point_multiplier = models.DecimalField(_("Point Multiplier"), max_digits=4, decimal_places=2, default=1.0) + discount_percentage = models.DecimalField(_("Discount Percentage"), max_digits=5, decimal_places=2, default=0) + color_code = models.CharField(_("Color Code"), max_length=20, default="#6c757d") + + def __str__(self): + return f"{self.name_en} / {self.name_ar}" + class Customer(models.Model): name = models.CharField(_("Name"), max_length=200) phone = models.CharField(_("Phone"), max_length=20, blank=True) email = models.EmailField(_("Email"), blank=True) address = models.TextField(_("Address"), blank=True) + loyalty_points = models.DecimalField(_("Loyalty Points"), max_digits=15, decimal_places=2, default=0) + loyalty_tier = models.ForeignKey(LoyaltyTier, on_delete=models.SET_NULL, null=True, blank=True, related_name="customers") def __str__(self): return self.name + def update_tier(self): + tiers = LoyaltyTier.objects.filter(min_points__lte=self.loyalty_points).order_by('-min_points') + if tiers.exists(): + self.loyalty_tier = tiers.first() + self.save() + +class LoyaltyTransaction(models.Model): + TRANSACTION_TYPES = [ + ('earned', _('Earned')), + ('redeemed', _('Redeemed')), + ('adjusted', _('Adjusted')), + ] + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="loyalty_transactions") + sale = models.ForeignKey('Sale', on_delete=models.SET_NULL, null=True, blank=True, related_name="loyalty_transactions") + transaction_type = models.CharField(_("Type"), max_length=20, choices=TRANSACTION_TYPES) + points = models.DecimalField(_("Points"), max_digits=15, decimal_places=2) + notes = models.TextField(_("Notes"), blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.transaction_type} {self.points} for {self.customer.name}" + class Supplier(models.Model): name = models.CharField(_("Name"), max_length=200) contact_person = models.CharField(_("Contact Person"), max_length=200, blank=True) @@ -86,6 +123,8 @@ class Sale(models.Model): paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0) balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0) discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0) + loyalty_points_redeemed = models.DecimalField(_("Loyalty Points Redeemed"), max_digits=15, decimal_places=2, default=0) + loyalty_discount_amount = models.DecimalField(_("Loyalty Discount"), max_digits=15, decimal_places=3, default=0) payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash') status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') due_date = models.DateField(_("Due Date"), null=True, blank=True) @@ -290,6 +329,32 @@ class SystemSetting(models.Model): logo = models.ImageField(_("Logo"), upload_to="business_logos/", blank=True, null=True) vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True) registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True) + + # Loyalty Settings + loyalty_enabled = models.BooleanField(_("Enable Loyalty System"), default=False) + points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0) + currency_per_point = models.DecimalField(_("Currency Value per Point"), max_digits=10, decimal_places=3, default=0.010) + min_points_to_redeem = models.PositiveIntegerField(_("Minimum Points to Redeem"), default=100) def __str__(self): - return self.business_name \ No newline at end of file + return self.business_name + +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") + image = models.ImageField(_("Profile Picture"), upload_to="profile_pics/", blank=True, null=True) + phone = models.CharField(_("Phone Number"), max_length=20, blank=True) + bio = models.TextField(_("Bio"), blank=True) + + def __str__(self): + return self.user.username + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + if not hasattr(instance, 'profile'): + UserProfile.objects.create(user=instance) + instance.profile.save() diff --git a/core/templates/base.html b/core/templates/base.html index 61ca302..96b77d5 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -201,18 +201,39 @@
@@ -255,6 +283,159 @@
+ + +
+
+
+
{% trans "Loyalty Tiers" %}
+ +
+
+
+ + + + + + + + + + + + {% for tier in loyalty_tiers %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Tier Name" %}{% trans "Min. Points" %}{% trans "Multiplier" %}{% trans "Discount" %}{% trans "Actions" %}
+
+
+ {{ tier.name_en }} / {{ tier.name_ar }} +
+
{{ tier.min_points }}{{ tier.point_multiplier }}x{{ tier.discount_percentage }}% + + +
+
+ + {% trans "No loyalty tiers defined." %} +
+
+
+
+
+
+ + + + + @@ -337,8 +518,6 @@ const data = await response.json(); if (data.success) { - // Refresh table or append row (for simplicity, we suggest a reload or partial update) - // For a smooth experience, let's append the row const tableBody = document.querySelector('#paymentMethodsTable tbody'); const noItems = document.getElementById('noPaymentMethods'); if (noItems) noItems.parentElement.remove(); @@ -359,16 +538,14 @@ `; tableBody.appendChild(newRow); - // Success Toast/Alert (Simplified) alert('{% trans "Payment method added successfully!" %}'); if (stayOpen) { - // Clear fields document.getElementById('addPmNameEn').value = ''; document.getElementById('addPmNameAr').value = ''; } else { bootstrap.Modal.getInstance(document.getElementById('addPaymentModal')).hide(); - window.location.reload(); // Reload to get modals for the new row + window.location.reload(); } } else { alert('Error: ' + data.error); @@ -383,4 +560,4 @@ document.getElementById('saveAndAddAnotherPm').addEventListener('click', () => savePaymentMethod(true)); }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 13e9226..733c743 100644 --- a/core/urls.py +++ b/core/urls.py @@ -10,6 +10,7 @@ urlpatterns = [ path('purchases/', views.purchases, name='purchases'), path('reports/', views.reports, name='reports'), path('settings/', views.settings_view, name='settings'), + path('profile/', views.profile_view, name='profile'), path('users/', views.user_management, name='user_management'), path('api/group-details//', views.group_details_api, name='group_details_api'), @@ -95,4 +96,10 @@ urlpatterns = [ path('settings/payment-methods/edit//', views.edit_payment_method, name='edit_payment_method'), path('settings/payment-methods/delete//', views.delete_payment_method, name='delete_payment_method'), path('api/add-payment-method-ajax/', views.add_payment_method_ajax, name='add_payment_method_ajax'), + + # Loyalty + path('settings/loyalty/add/', views.add_loyalty_tier, name='add_loyalty_tier'), + path('settings/loyalty/edit//', views.edit_loyalty_tier, name='edit_loyalty_tier'), + path('settings/loyalty/delete//', views.delete_loyalty_tier, name='delete_loyalty_tier'), + path('api/customer-loyalty//', views.get_customer_loyalty_api, name='get_customer_loyalty_api'), ] diff --git a/core/views.py b/core/views.py index dce2c94..42dcb7a 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,4 @@ +import decimal from django.contrib.auth.models import User, Group, Permission from django.urls import reverse import random @@ -14,7 +15,7 @@ from .models import ( SaleItem, SalePayment, SystemSetting, Quotation, QuotationItem, SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, - PaymentMethod, HeldSale + PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction ) import json from datetime import timedelta @@ -90,6 +91,7 @@ def pos(request): customers = Customer.objects.all() categories = Category.objects.all() payment_methods = PaymentMethod.objects.filter(is_active=True) + settings = SystemSetting.objects.first() # Ensure at least Cash exists if not payment_methods.exists(): @@ -100,7 +102,8 @@ def pos(request): 'products': products, 'customers': customers, 'categories': categories, - 'payment_methods': payment_methods + 'payment_methods': payment_methods, + 'settings': settings } return render(request, 'core/pos.html', context) @@ -305,11 +308,23 @@ def create_sale_api(request): payment_method_id = data.get('payment_method_id') due_date = data.get('due_date') notes = data.get('notes', '') + + # Loyalty data + points_to_redeem = data.get('loyalty_points_redeemed', 0) customer = None if customer_id: customer = Customer.objects.get(id=customer_id) + settings = SystemSetting.objects.first() + if not settings: + settings = SystemSetting.objects.create() + + loyalty_discount = 0 + if settings.loyalty_enabled and customer and points_to_redeem > 0: + if customer.loyalty_points >= points_to_redeem: + loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point) + sale = Sale.objects.create( customer=customer, invoice_number=invoice_number, @@ -317,6 +332,8 @@ def create_sale_api(request): paid_amount=paid_amount, balance_due=float(total_amount) - float(paid_amount), discount=discount, + loyalty_points_redeemed=points_to_redeem, + loyalty_discount_amount=loyalty_discount, payment_type=payment_type, due_date=due_date if due_date else None, notes=notes, @@ -359,9 +376,36 @@ def create_sale_api(request): product.stock_quantity -= int(item['quantity']) product.save() - settings = SystemSetting.objects.first() - if not settings: - settings = SystemSetting.objects.create() + # Handle Loyalty Points + if settings.loyalty_enabled and customer: + # Earn Points + points_earned = float(total_amount) * float(settings.points_per_currency) + if customer.loyalty_tier: + points_earned *= float(customer.loyalty_tier.point_multiplier) + + if points_earned > 0: + customer.loyalty_points += decimal.Decimal(str(points_earned)) + LoyaltyTransaction.objects.create( + customer=customer, + sale=sale, + transaction_type='earned', + points=points_earned, + notes=f"Points earned from Sale #{sale.id}" + ) + + # Redeem Points + if points_to_redeem > 0: + customer.loyalty_points -= decimal.Decimal(str(points_to_redeem)) + LoyaltyTransaction.objects.create( + customer=customer, + sale=sale, + transaction_type='redeemed', + points=-points_to_redeem, + notes=f"Points redeemed for Sale #{sale.id}" + ) + + customer.update_tier() + customer.save() return JsonResponse({ 'success': True, @@ -755,6 +799,12 @@ def settings_view(request): settings.vat_number = request.POST.get('vat_number') settings.registration_number = request.POST.get('registration_number') + # Loyalty Settings + settings.loyalty_enabled = request.POST.get('loyalty_enabled') == 'on' + settings.points_per_currency = request.POST.get('points_per_currency', 1.0) + settings.currency_per_point = request.POST.get('currency_per_point', 0.010) + settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100) + if 'logo' in request.FILES: settings.logo = request.FILES['logo'] @@ -763,10 +813,12 @@ def settings_view(request): return redirect(reverse('settings') + '#profile') payment_methods = PaymentMethod.objects.all() + loyalty_tiers = LoyaltyTier.objects.all().order_by('min_points') return render(request, 'core/settings.html', { 'settings': settings, 'payment_methods': payment_methods, + 'loyalty_tiers': loyalty_tiers }) @login_required @@ -1315,7 +1367,6 @@ def add_customer_ajax(request): return JsonResponse({'success': False, 'error': str(e)}, status=400) return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) -@csrf_exempt @login_required def hold_sale_api(request): if request.method == 'POST': @@ -1357,7 +1408,6 @@ def get_held_sales_api(request): }) return JsonResponse({'success': True, 'held_sales': data}) -@csrf_exempt @login_required def recall_held_sale_api(request, pk): held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user) @@ -1371,9 +1421,111 @@ def recall_held_sale_api(request, pk): held_sale.delete() return JsonResponse(data) -@csrf_exempt @login_required def delete_held_sale_api(request, pk): held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user) held_sale.delete() - return JsonResponse({'success': True}) \ No newline at end of file + return JsonResponse({'success': True}) + +@login_required +def add_loyalty_tier(request): + if request.method == 'POST': + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + min_points = request.POST.get('min_points', 0) + multiplier = request.POST.get('point_multiplier', 1.0) + discount = request.POST.get('discount_percentage', 0) + color = request.POST.get('color_code', '#6c757d') + + LoyaltyTier.objects.create( + name_en=name_en, name_ar=name_ar, + min_points=min_points, point_multiplier=multiplier, + discount_percentage=discount, color_code=color + ) + messages.success(request, "Loyalty tier added successfully!") + return redirect(reverse('settings') + '#loyalty') + +@login_required +def edit_loyalty_tier(request, pk): + tier = get_object_or_404(LoyaltyTier, pk=pk) + if request.method == 'POST': + tier.name_en = request.POST.get('name_en') + tier.name_ar = request.POST.get('name_ar') + tier.min_points = request.POST.get('min_points') + tier.point_multiplier = request.POST.get('point_multiplier') + tier.discount_percentage = request.POST.get('discount_percentage') + tier.color_code = request.POST.get('color_code') + tier.save() + messages.success(request, "Loyalty tier updated successfully!") + return redirect(reverse('settings') + '#loyalty') + +@login_required +def delete_loyalty_tier(request, pk): + tier = get_object_or_404(LoyaltyTier, pk=pk) + tier.delete() + messages.success(request, "Loyalty tier deleted successfully!") + return redirect(reverse('settings') + '#loyalty') + +@login_required +def get_customer_loyalty_api(request, pk): + customer = get_object_or_404(Customer, pk=pk) + settings = SystemSetting.objects.first() + + tier_info = None + if customer.loyalty_tier: + tier_info = { + 'name_en': customer.loyalty_tier.name_en, + 'name_ar': customer.loyalty_tier.name_ar, + 'multiplier': float(customer.loyalty_tier.point_multiplier), + 'discount': float(customer.loyalty_tier.discount_percentage), + 'color': customer.loyalty_tier.color_code + } + + return JsonResponse({ + 'success': True, + 'points': float(customer.loyalty_points), + 'tier': tier_info, + 'currency_per_point': float(settings.currency_per_point) if settings else 0.01, + 'min_points_to_redeem': settings.min_points_to_redeem if settings else 100 + }) + +@login_required +def profile_view(request): + """ + User Profile View + """ + if request.method == 'POST': + user = request.user + user.first_name = request.POST.get('first_name') + user.last_name = request.POST.get('last_name') + user.email = request.POST.get('email') + + # Profile specific + profile = user.profile + profile.phone = request.POST.get('phone') + profile.bio = request.POST.get('bio') + + if 'image' in request.FILES: + profile.image = request.FILES['image'] + + user.save() + profile.save() + + # Password change + password = request.POST.get('password') + confirm_password = request.POST.get('confirm_password') + if password: + if password == confirm_password: + user.set_password(password) + user.save() + from django.contrib.auth import update_session_auth_hash + update_session_auth_hash(request, user) + messages.success(request, "Profile and password updated successfully!") + else: + messages.error(request, "Passwords do not match.") + else: + messages.success(request, "Profile updated successfully!") + + return redirect('profile') + + return render(request, 'core/profile.html') diff --git a/static/css/custom.css b/static/css/custom.css index d5cb873..cb28598 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -193,4 +193,117 @@ body { #content { width: 100%; } -} \ No newline at end of file +} + +/* Enhanced Responsiveness */ + +/* Container & Spacing Adjustments */ +@media (max-width: 576px) { + main.p-4 { + padding: 1rem !important; + } + .hero-gradient { + padding: 1.5rem; + } + .top-navbar { + padding: 10px 15px; + } + h1, .h1 { font-size: 1.5rem; } + h2, .h2 { font-size: 1.25rem; } + h3, .h3 { font-size: 1.1rem; } + h4, .h4 { font-size: 1rem; } +} + +/* Sidebar Overlay for Mobile */ +@media (max-width: 992px) { + #sidebar { + box-shadow: 0 0 20px rgba(0,0,0,0.1); + } + #sidebar.active { + margin-inline-start: 0; + } + #sidebar:not(.active) { + margin-inline-start: calc(-1 * var(--sidebar-width)); + } + + /* Overlay effect when sidebar is active */ + #sidebar.active::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0,0,0,0.2); + z-index: -1; + display: block; + } +} + +/* Table Card Styling for Mobile */ +@media (max-width: 768px) { + .table-responsive { + border-radius: 12px; + overflow: hidden; + } + .btn-sm { + padding: 0.4rem 0.8rem; + } +} + +/* POS Specific Responsive Utilities */ +.mobile-cart-toggle { + display: none; + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1030; + border-radius: 50px; + padding: 12px 24px; + box-shadow: 0 4px 15px rgba(46, 91, 255, 0.4); +} + +[dir="rtl"] .mobile-cart-toggle { + right: auto; + left: 20px; +} + +@media (max-width: 991px) { + .mobile-cart-toggle { + display: flex; + align-items: center; + } + .cart-container { + position: fixed !important; + top: 0 !important; + right: -100% !important; + width: 100% !important; + height: 100% !important; + z-index: 1040 !important; + transition: right 0.3s ease; + border-radius: 0 !important; + } + [dir="rtl"] .cart-container { + right: auto !important; + left: -100% !important; + transition: left 0.3s ease; + } + .cart-container.show { + right: 0 !important; + } + [dir="rtl"] .cart-container.show { + left: 0 !important; + } + + /* Ensure body doesn't scroll when cart is open */ + body.cart-open { + overflow: hidden; + } +} + +/* Grid adjustments */ +@media (max-width: 400px) { + #productGrid .col { + width: 100% !important; + } +}